ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

多线程学习笔记

2021-01-18 22:02:49  阅读:88  来源: 互联网

标签:Thread 笔记 学习 线程 println new 多线程 public out


基本概念

程序(program):一段静态的代码

进程(process):已经加载到内存中正在运行的程序, 是动态的;进程是资源分配的基本单位

  • 如正在执行的一个qq、微信程序就是一个进程

线程(thread):进程可进一步划分为线程,是一个程序内部的一条执行路径

  • 多线程:一个进程同一时间并行执行多个线程,就是支持多线程的
  • 线程是调度执行的基本单位,每个线程拥有独立的运行栈和程序计数器
  • 一个进程中的多个线程共享相同的内存单元/内存地址空间,他们从同一堆中分配对象,可以访问相同的变量和对象,这使得线程间的通信更简单,但是共享资源也会带来安全隐患。
  • 360安全卫士,打开电脑体检、木马查杀,电脑清理–这就是多线程运行了

单核CPU与多核CPU:单核CPU使用多线程的时候是一种假的多线程,因为在一个时间单元内,只能执行一个线程的任务,它是交替着执行的,表现出多线程的假象。多核CPU才能更好的发挥多线程的效率。

一个Java程序至少有三个线程:main()主线程、gc()垃圾回收线程、异常处理线程。如果发生有异常,会影响主线程。

java程序启动的时候,实际上是启动了一个JVM进程,然后,JVM启动主线程执行main()方法,在main()方法中,可以启动其他线程。

并行与并发

  • 并行:多个CPU同时执行多个任务,比如:多个人同时做不同的事
  • 并发:一个CPU同时执行多个任务,比如:秒杀、多个人做同一件事

什么时候需要用到多线程?

  • 程序需要同时执行两个或者多个任务
  • 程序需要实现一些需要等待的任务,如用户输入、文件读写操作、网络操作、搜索等
  • 需要一些后台运行的程序

线程的创建和使用

创建方式一共有四种。

方式一:继承Thread类,然后重写run方法,调用该实例的run方法开启线程

代码示例:

public class MyThread extends Thread {
    
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();   //start方法会自动调用实例的run()方法
    }

    @Override
    public void run() {
        System.out.println("创建了一个新的线程!");
    }
}

输出结果:

创建了一个新的线程!

方式二:创建Thread实例时,传入一个Runnable实例

public class MyThread {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("创建了一个新的线程");     
            }
        });
        t.start();
    }
}

//使用lambda表达式简化
public class MyThread {
    public static void main(String[] args) {
        Thread t = new Thread(() ->
                System.out.println("创建了一个新的线程")
        );
        t.start();
    }
}

//输出结果
创建了一个新的线程

多线程运行的一个实例:

public class MyThread {
    public static void main(String[] args) {
        System.out.println("main start...");
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread run...");
                try {
                    Thread.sleep(10);	//让新线程睡眠10毫秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("thread end...");
            }
        });
        t.start();	//从这里开始 开启了一个新线程
        System.out.println("main end...");
    }
}
//输出结果
main start...
main end...
thread run...
thread end...

继承Thread类和实现Runnable接口两种方式的对比:

优先选择实现Runnable接口的方式,原因:

1.实现的方式没有类的单继承的局限性

2.实现的方式更适合处理多个线程有共享数据的情况

联系:

Thread类也实现了Runnable接口。两种方式都需要重写run方法,将创建的线程要执行的逻辑声明在run方法中。

方式三:实现Callable接口

//1.创建一个Callable的实现类
class NumThread implements Callable {
    //2.实现call方法,将此线程需要执行的操作声明在call方法中
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                sum += i;
                System.out.println(i);
            }
        }
        return sum;
    }
}

public class ThreadNew {
    public static void main(String[] args) {
        //3.创建Callable接口实现类的对象
        NumThread numThread = new NumThread();
        //4.将此Callable接口实现类的对象作为参数残敌到FutureTask构造器中,创建FutureTask的对象
        FutureTask futureTask = new FutureTask(numThread);
        //5.将此FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()方法启动线程
        new Thread(futureTask).start();

        Object sum = null;
        try {
            //6.获取Callable中call方法的返回值
            //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值
            sum = futureTask.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("总和为:sum = " + sum);

    }
}

实现Callable接口比实现Runnable接口更强大:

1.call()可以有返回值

2.call()可以抛出异常,被外面的操作捕获

3.Callable支持泛型

方式四:使用线程池

线程池:提前创建好多个线程,放入线程池当中,使用完毕后放回池中,可以避免频繁创建销毁、实现重复利用。

优点:

  • 提高响应速度
  • 降低资源消耗
  • 便于线程管理

线程的状态

线程有六种状态:

  1. New:新创建的线程,还没有执行
  2. Runnable:运行中的线程,正在执行run()方法
  3. Blocked:运行中的线程,但因为某些操作被阻塞而挂起了
  4. Waiting:运行中的进程,但因为某些操作在等待中
  5. Timed Waiting:运行中的线程,由于执行sleep()方法正在计时等待
  6. Terminated:线程已终止,因为run()方法执行完毕

线程终止的原因:

  • 正常终止:执行到run()方法的return语句
  • 意外终止:run()方法由于没有捕获的异常而终止
  • 对Thread的实例调用stop()方法强制终止(不推荐

一个线程可以等待另一个线程直至其运行结束,例如main线程等待t线程结束后再运行,通过t.join()实现

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            System.out.println("hello");
        });
        System.out.println("start");
        t.start();
        t.join();   //main线程会等待t线程结束后再运行
        System.out.println("end");
    }
}

//运行结果
start
hello
end

##中断线程

中断线程就是其他线程给该线程发一个中断信号,该线程接收到中断信号后结束执行run方法,使得线程结束运行。

使用interrupt()方法实现中断目标线程,目标线程通过检查自身状态判断是否有中断请求。

中断程序:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        MyThread t = new MyThread();
        t.start();
        Thread.sleep(3);    //让main线程睡一会,要不然会直接打印end
        t.interrupt();  //中断t线程
        t.join();   //等到t线程结束
        System.out.println("end");
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        int n = 0;
        while (!isInterrupted()) {
            n++;
            System.out.println(n + " hello!");
        }
    }
}
//输出结果
1 hello!
end

t.join()会让main线程进入等待状态,此时,如果对main线程调用interrupt(),join()方法会抛出InterruptedException异常。

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread();
        t.start();  //开启t线程
        Thread.sleep(1000);
        t.interrupt();  //中断t线程,让t结束
        t.join();   //等待t线程结束,但是t处于等待状态,所以该方法会抛出InterruptedException,并立刻结束t线程
        System.out.println("end");
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        HelloThread hello = new HelloThread();
        hello.start();  //开启hello线程
        try {
            hello.join();   //等待hello线程结束,此时MyThread线程处于等待状态
        } catch (InterruptedException e) {
            System.out.println("interrupted!");
        }
        hello.interrupt();
    }
}

class HelloThread extends Thread {
    @Override
    public void run() {
        int n = 0;
        while (!isInterrupted()) {
            n++;
            System.out.println(n + " hello!");
            try {
                Thread.sleep(100);
                //如果对处于等待状态的hello线程调用interrupt()会让怕抛出InterruptedException异常
            } catch (InterruptedException e) {
                break;  //捕获到InterruptedException就退出,即结束线程
            }
        }
    }
}
//运行结果
1 hello!
2 hello!
3 hello!
4 hello!
5 hello!
6 hello!
7 hello!
8 hello!
9 hello!
10 hello!
interrupted!
end

还可以通过设置标志位中断线程。代码演示:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        MyThread t = new MyThread();
        t.start();
        Thread.sleep(1);
        t.running = false;
    }
}


class MyThread extends Thread {
    public volatile boolean running = true;
    @Override
    public void run() {
        int n = 0;
        while (running) {
            n++;
            System.out.println(n + " hello");
        }
        System.out.println("end");
    }
}

//输出结果
1 hello
end

重点在volatile关键字:解决了共享变量在线程间的可见性问题。

running是一个线程共享的变量,需要使用voltile关键字标记,确保每个线程都能读取到更新后的变量值。

在java虚拟机中,变量的值保存在主内存中,但是当线程访问变量的时候,它会先获取一个副本,并保存在自己的工作内存中,如果线程修改了变量的值,虚拟机会在某个时候把修改后的变量回写到主内存中,但是这个时间是不确定的。这就导致了如果一个线程更新了某个变量,另一个线程读取到的值还是未修改之前的值,因此volatile关键字的作用是:

  • 每次访问变量时,总是获取主内存中的最新值
  • 每次修改变量后,立刻回写到主内存

同步机制

同步机制解决线程安全问题。

synchronized(锁对象) {
    //同步代码块,即操作共享数据的代码块
    //在同步代码块里面最多只有一个线程在执行,其他线程等待。相当于是一个单线程的过程,效率较低
}

注意:
    1.多个线程必须共用一把锁
    2.锁可以是任何一个对象

线程死锁

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。

出现死锁后,不会出现异常和提示,只是所有的线程都处于阻塞状态,无法继续。

避免死锁:

  • 专门的算法、原则
  • 尽量减少同步资源的定义
  • 尽量避免嵌套同步
/**
 * 线程死锁
 */
public class ThreadTest {

    public static void main(String[] args) {
        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();

        new Thread(){
            @Override
            public void run() {
                synchronized (s1) {
                    s1.append("a");
                    s2.append(1);

                    //这个线程阻塞的这段时间内,只要执行了下面这个线程,必定会出现死锁
                    //因为s1这把锁这个线程拿着呢,下面那个线程无法获得
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    synchronized (s2) {
                        s1.append("b");
                        s2.append(2);
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (s2) {

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    s1.append("c");
                    s2.append(3);
                    synchronized (s1) {
                        s1.append("d");
                        s2.append(4);
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }).start();
    }
}

Lock

手动上锁解锁解决线程安全问题

代码:

/**
 * Lock
 */
public class LockTest {

    public static void main(String[] args) {
        Window w = new Window();

        Thread t1 = new Thread(w, "窗口1");
        Thread t2 = new Thread(w, "窗口2");
        Thread t3 = new Thread(w, "窗口3");
        t1.start();
        t2.start();
        t3.start();
    }

}

class Window implements Runnable {

    private int ticket = 100;

    //1.创建锁对象
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {

            try {
                //2.调用加锁方法lock
                lock.lock();
                
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ": 卖票,票号为:  " + ticket);
                    ticket--;
                } else {
                    break;
                }
            } finally {
                //3.调用解锁方法unclock()
                lock.unlock();
            }


        }
    }
}

synchronized 和 Lock 的异同?

相同:二者都可以解决线程安全问题。

不同:synchronized机制在执行完相应的同步代码块之后,自动释放同步监视器。

​ Lock需要手动启动同步(lock)、结束同步(unlock),使用的时候能够更灵活一些。

线程通信

主要使用几个方法:

wait():执行该方法,当前线程进入阻塞状态,并释放同步监视器

notify():执行该方法,会唤醒被wait的一个线程。如果多个线程被wait,就唤醒优先级高的线程

nitifyAll():执行该方法,会唤醒所有被wait的线程

注意:

1.wait()、notify()、notifyAll()三个方法必须使用在同步代码块或同步方法中

2.wait()、notify()、notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器(锁),否则会出现异常

3.wait()、notify()、notifyAll()三个方法是定义在Object类中的

sleep和wait方法的异同?

相同点:执行方法都可以使当前线程进入阻塞状态

不同点:

1.声明的位置不同:sleep()在Thread类中声明;wait()在Object类中声明

2.调用要求不同:sleep()可以在任何需要的场景下调用;wait()必须使用在同步代码块或同步方法中

3.如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁

标签:Thread,笔记,学习,线程,println,new,多线程,public,out
来源: https://blog.csdn.net/qq_30634979/article/details/112795863

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有