• 死磕Java基础--Java多线程那点事儿


    1、线程和进程的区分

    一个进程中包含多个线程,一个进程就相当于一个应用程序,一个应用程序底层就是cpu来运行的,比如我们的电脑同时打开了多个应用,表面看来像是在同时运行,实际上在同一时间只运行了一个应用程序,只不过cpu的运行速度非常快,会进行高速切换,让我们觉得是在同时进行。

    最经典的一个例子就是迅雷了,我们电脑打开迅雷相当于开了一个进程,当我们使用迅雷下载东西的时候,比如说,下载两部电影,那么在迅雷中就存在两个不同的执行路径,也就是有两个线程在同时做下载工作。

    所以,进程包含线程,相当于所有线程的集合。一个线程就是一个执行路径。

    为什么要用多线程?
    多线程的好处就是提高程序的效率。但是可能会影响性能。

    main是主线程,我们创建的线程叫做子线程,如果说一个项目中肯定有一个线程,那么这个线程就是主线程了。

    image

    对于这个问题,到底该怎么理解或者说区分线程和进程的概念,再举一个非常贴切的例子,我们大多数人都用过扣扣吧,我们打开一个扣扣,其实就是开启了一个进程,然后我们发送一段文字,那就是开启了一个线程,我们再发送一天语音,那就是又开启了一个线程,那么在这个扣扣的进程中就有发文字和语言两个线程了,当然,可能还有其他的线程!

    在知乎上有这么一个帖子,就是区分线程和进程的额,回答的也不少,可以看看

    线程和进程的区别是什么?

    那么,关于进程和线程的区别问题就到这!

    2、多线程的创建方式

    学习多线程,最基本的就是要会创建多线程了,常规来说创建多线程的方式应该有三种

    1. 继承Thread类
    2. 实现Runnable接口
    3. 匿名内部类

    在此之前很有必要说一下这个main,也就是在写Java程序中经常见到的主线程,代码表现形式就是

        public static void main(String[] args){
            do...
        }

    这个main叫做主线程,是程序的入口,而且是由jvm也就是Java虚拟机创建的。

    下面具体说一下创建线程的三种方式

    首先是继承自Thread类的方式,看代码

    /**
     * 创建线程的我第一种方式
     * 继承自Thread类
     */
    class A extends Thread{
        @Override
        public void run() {
            System.out.println("正在执行线程A。。。。");
        }
    
    }

    以上就是使用继承自Thread类的方式创建线程,这里的Thread实际上是实现了Runnable接口

    image

    再看这个Runnable接口

    image

    因此,使用继承Thread类的方式创建线程需要实现run方法,实际的逻辑处理也是在这个run方法中实现的

    再看第二种创建线程的方式就是实现Runnable接口的方式,同样,先来看代码

    /**
     * 创建线程的第二种方式
     * 实现Runnable接口
     */
    class B implements Runnable {
    
        @Override
        public void run() {
            System.out.println("正在执行线程B、、、");
        }
    }

    之前就说过,Runnable接口中有一个抽象run方法,所以,对于实现Runnable接口的方式也是需要实现run方法的,同样的逻辑处理也是在run中,接下来看最后一种创建线程的方式,通过匿名内部类的方式。

        public static void main(String[] args) {
            System.out.println("主线程在执行、、、、");
    
    
            /**
             * 线程创建的第三种方式
             * 匿名内部类
             */
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("匿名内部类执行的线程");
                }
            }).start();
    
        }

    这里要注意,匿名内部类是要写在方法之中的,这里写在主方法中,可以看到,这个线程是通过新建一个Thread对象,然后在传入一个Runnable,之后也是实现Run方法,然后调用线程的start方法即可开启此线程

    在最后一种使用匿名内部类的方式创建线程中调用了start开启线程,那么,对于其他两种创建线程的方式该如何启动线程呢?

     //执行线程B
            B b = new B();
            Thread thread = new Thread(b);
            thread.start();
    
            //执行线程A
            A a = new A();
            a.start();

    可以看到,都是调用线程对象的start方法从而开启线程,这里有些人会有些疑问,我们随便看一个

    
    /**
     * 创建线程的我第一种方式
     * 继承自Thread类
     */
    class A extends Thread{
        @Override
        public void run() {
            System.out.println("正在执行线程A。。。。");
        }
    
    }

    就拿这个线程来说,为什么不可以这样

    image

    也就是说在线程对象中是有一个run方法的,为什么执行线程不可以直接调用这个run方法呢?而要调用start开启线程呢?

    其实也很好理解,如果直接调用run方法的情况下,那么这跟平常的类又有什么区别呢?要知道线程是独立的额一个线程,如果直接调用run方法的话不就等同于直接执行这个方法,就类似一个普通的类,然后调用它的一个方法似的,可是,这里可是线程啊。

    说的官方一点,对于线程而言,有一个线程规划器的概念,可以理解为就是专门管理线程执行的一个玩意,只有当你调用start,才会将这个线程交给线程规划器去管理,只有交给了线程规划器,才能真正算得上是一个线程,否则,就是一个普通的类。而非线程。

    以上说了创建线程的三种方式,那么,到底使用哪种比较好呢?实际情况中可能使用实现Runnable接口的方式可能多一点,为什么呢?

    也很简单,因为在Java中,类只能是单继承的,所以如果使用继承Tread类的方式的话就不能再继承自其它的类了,这在实际的开发中势必会带来一些局限性,但是使用实现Runnable接口的方式就可以避免这一局限性,因为接口是支持多实现的,而且还可以再继承其它的类,这样的话,灵活性就高出很多。

    到此要知道的几个知识点

    1. 创建线程的三种方式
    2. 为什么不调用run
    3. 使用哪种创建线程的方式更好,为什么

    再续

    3、线程常用的API

    先来看一段代码

    image

    在这段代码中,跟之前写的创建线程代码没什么区别,就是在打印的时候添加了一个

    Thread.currentThread().getName()

    很好理解,就是得到当前线程的名称的,看输出结果

    image

    平常在开发当中,如果需要得知当前线程就可以使用此方法来获得当前线程的名称。

    下面再介绍另外一个方法:isAlive()

    这个方法是用来判断当前线程是否处于活动状态,那么首先需要知道什么是“活动状态”

    所谓的活动状态就是线程已经启动且尚未停止,根据这个理解,看一段代码,以一个线程为例

    /**
     * 创建线程的第一种方式
     * 继承自Thread类
     */
    class A extends Thread{
        @Override
        public void run() {
            System.out.println("正在执行线程A。。。。"+Thread.currentThread().getName());
            System.out.println("线程的活动状态是:"+Thread.currentThread().isAlive());
    
        }
    
    }

    然后执行

     //执行线程A
            A a = new A();
            System.out.println("此时线程的状态是:"+Thread.currentThread().isAlive());
            a.start();
            System.out.println("此时线程的状态是:"+Thread.currentThread().isAlive());

    重点看一下执行结果

    image

    对照着代码再看执行结果,能够看出对于线程a来说只有当调用了start方法,线程才开始执行,也就是处于活动状态。

    此外还有一个比较熟悉的方法就是sleep(),是让线程暂时休眠的一个方法,这里要注意的是是让当前正在执行的线程暂停执行,下面看一个具体的例子

         //执行线程A
            A a = new A();
            a.start();
            System.out.println(System.currentTimeMillis());
            try {
                Thread.sleep(2000);
                System.out.println(System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

    代码当中添加了以下代码来让线程休眠2秒

    hread.sleep(2000);

    然后看打印输出结果

    image

    线程在执行的过程中暂停了2000毫秒也就是2秒钟的时间,这在平常的开发中也有一些特殊的用处,需要用到的时候能够写出来即可。

    以上都是在介绍线程的一些常用API,其实还有一个也应该知晓,那就是getId(),这个是用来活的线程的唯一标示的,比如有如下用法

    image

    看打印输出结果

    image

    得出的线程ID则可以作为判定此线程的唯一标示

    关于线程的常用API就介绍以上这几个,更多的可以等到需要用到的时候再针对的去查询,对于以上常用的则需要记住。

    4、线程的停止

    对于线程,它有这样的生命周期,就是新建、就绪、运行、阻塞和消亡

    对于这几种状态也比较好理解,首先是

    1. 新建状态:没有调用satrt方法就处于新建状态
    2. 就绪状态:即使此时已经调用了start的方法,线程也不会立马执行,必须等到jvm调用run方法线程才会真正的执行,而当前状态则为就绪状态
    3. 运行状态:就是调用了run方法之后
    4. 阻塞状态:在运行状态如果调用了sleep方法就会处于阻塞状态
    5. 消亡状态:也就是线程被停止了

    关于线程的这几种状态,要好好说一说的就是线程的停止了,因为关于线程的停止不是想象中的那样,也许可以调用线程的stop方法将线程进行停止掉,但是,现如今,stop已经不推荐使用了,大多数停止一个线程将采用Thread.interrupt()这个方法。

    而关于interript也不是想象中的那样,只要调用了这个方法,线程就会停止,其实,调用了interrupt只相当于给当前线程打上了一个停止的标记,而此时,线程其实并没有真正的停止,而这其中很明显,缺少一些步骤。

    来看一段代码

    /**
     * 创建线程的我第一种方式
     * 继承自Thread类
     */
    class A extends Thread{
        @Override
        public void run() {
            System.out.println("正在执行线程A。。。。"+Thread.currentThread().getName());
            for (int i=0;i<10;i++){
                System.out.println(i);
    
            }
    
        }
    
    }
    
    //执行线程A
            A a = new A();
            a.start();
            a.interrupt();

    如果看到上面的代码,会不会以为线程会被停止掉呢?实际答案是不会,以上并没有真正的去停止线程,而是打上了一个停止的标记,那该怎么做,这里需要加上一个判断

    也就是说,有了如下执行线程的代码

       //执行线程A
            A a = new A();
            a.start();
            //此处打上一个停止的标记
            a.interrupt();

    并且已经调用了interrupt,相当于已经给此线程打上了中断的标志,但是此时线程并没有停止,还需要做如下的判断

    image

    这里使用到了这么一句代码

    this.isInterrupted()

    这行代码代表着获取线程的中断标志,简单来说,如果在此之前你调用了interrupt的话它就返回true,否则就是false,所以就可以通过这种方式来达到停止线程的目的。

    这个isInterrupted就是返回线程是否中断的一个状态,看执行结果

    image

    此时线程的中断状态是true,再看下面这种情况

    image

    这里有个很重要的知识点,先看下输出结果吧

    image

    可以看到,此时的中断状态变成了false,在之前明明已经为线程打上中断标志了,为什么这里变成了false,这是因为线程休眠的缘故,简单来说,如果你让线程进行休眠,就会抛出一个中断异常,因为你之前打上了中断标志,所以调用sleep就会抛出一个中断异常,而且还会将中断标志设置成false,对,就是这么个道理,所以这里的中断标志状态才会是false。

    继续回到之前成功停止线程的代码上,也就是这些

    
    /**
     * 创建线程的我第一种方式
     * 继承自Thread类
     */
    class A extends Thread{
        @Override
        public void run() {
            System.out.println("正在执行线程A。。。。"+Thread.currentThread().getName());
            for (int i=0;i<10;i++){
                if (this.isInterrupted()){
                    System.out.println("线程已经停止");
                    System.out.println("当前线程的状态:"+this.isInterrupted());
                    break;
                }
                System.out.println(i);
    
            }
        }
    
    }

    上面的做法貌似已经将线程停止掉了,但是事实真的是这样吗,小小的改动一下

    image

    看下打印结果

    image

    什么意思呢?也就是说在for循环之后的代码还是会执行的,这样来看,这个线程好像就没有真的被停止掉,那么,该怎么处理这种情况呢?

    /**
     * 创建线程的我第一种方式
     * 继承自Thread类
     */
    class A extends Thread{
        @Override
        public void run() {
            System.out.println("正在执行线程A。。。。"+Thread.currentThread().getName());
            try {
                for (int i=0;i<10;i++){
                    if (this.isInterrupted()){
                        System.out.println("线程已经停止");
                        System.out.println("当前线程的状态:"+this.isInterrupted());
                        throw new InterruptedException();
                    }
                    System.out.println(i);
                }
                System.out.println("此处还会被执行");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    

    以上就是一个处理方法,我们在判断到中断标志之后抛出一个中断异常,然后再捕获这个异常,这样就能避免for循环之后的鳄鱼局继续被执行的情况,也就是真正的停止掉线程。

    所以对于线程的停止,一个好的方法就是上面这种抛异常的方式了。

    5、线程安全问题(synchronized)

    提到线程安全问题,首先就要先回答一个问题,那就是为什么会出现线程安全问题呢?

    那么为什么会出现线程安全问题呢?简单来说,及时多个线程同时访问一个共享变量的情况下就会出现线程安全的问题,简单来个例子看一下

    class User{
    
        int age;
    
        public void setAge(int age0) {
            this.age = age0;
    System.out.println("age="+age+"当前线程为:"+Thread.currentThread().getName());
        }
    
    
    }
    
    public class SynchronizedTest {
    
        public static void main(String[] args) {
    
            User user = new User();
    
            Thread a = new Thread(new Runnable() {
                @Override
                public void run() {
                    //在线程a中对user的age及逆行数值修改
                    user.setAge(66);
    
                }
            });
    
            Thread b = new Thread(new Runnable() {
                @Override
                public void run() {
                    //在线程b中对user的age及逆行数值修改
                    user.setAge(88);
                }
            });
    
    
            //启动线程
            a.start();
            a.setName("线程a");
    
            b.start();
            b.setName("线程b");
    
        }
    }

    这个时候运行我们的代码可能会出现这样的问题

    image

    难道b线程中age不应该等于88吗?怎么都是66呢?这就出现了线程安全问题。

    还可能会出现这样的情况

    image

    这种情况就说明线程的实际执行顺序并不一定按照代码书写的顺序。

    而且还会出现这样的问题

    这里也是发生了线程安全问题,那么该如何解决这个线程安全问题呢?要解决这个问题还要明白两个概念,那就是同步和异步,那什么是同步什么又是异步呢?

    先来简单分析一下上述代码为什么会出现线程安全问题,其实很简单,对于age是共享内存,两个线程可以同时对它进行访问,当线程a访问它将它的数值修改成66的时候可能会出现的一种情况就是,线程a刚把age修改成66,线程b又把它修改成88了,导致读取到的都是88,也就是说线程a修改完成age之后被线程b打断了一下,没有及时的去读取到自己修改的值,而是读取到了被线程b修改的值。

    再想一下为什么会出现这种情况呢?其实就是在线程a调用setAge之后,线程b又调用了这个setAge,因此发生线程安全问题,此时这个方法就是异步的,也就是可以被两个线程同时操作,如果这个setAge被线程a调用期间线程b不能调用,只有等线程a调用并且完成相关操作,线程b才能够调用,此时这个setAge就是同步的,而且也不会发生线程安全问题了

    那么怎么实现上述所说的呢?

    可以这样解决

    image

    也就是使用synchronized来修饰setAge,这样的话当线程a调用setAge的时候就会把这个方法加锁,此时setAge是被锁住的,线程b是无法调用的,只有当线程a把setAge操作执行完成之后,锁才会被打开。

    这就就是接下来要说的使用synchronized来同步方法从而解决线程安全问题

    觉得以上的方法就是最优的了吗?当然不是,想一下使用synchronized来同步方法也就相当于给这个方法加上一个锁,不能同时被多个线程访问,但是如果这个方法中含有耗时操作而这个耗时操作又是不涉及线程安全的,那么使用synchronized来同步方法显然降低了性能,那该怎么解决这个问题呢?

    解决的一个思路就是只对引起线程安全的代码进行synchronized同步,可以这样做

    image

    这就是使用synchronized来同步发生线程安全的代码块,这里要注意synchronized需要传入一个对象,这个对象可以是任意对象,但是要保证这个对象是被多个线程共享的,如果把这个对象定义在了方法里,那么每个线程调用方法都会创建一个新的对象,如此一来,多个线程访问的就不是同一个对象,因此,依然发生线程安全问题,如下图代码操作就是错误的。

    image

    这里再说一下这个synchronized,在上面的代码中,为解决线程安全问题,在setAge方法上加上了一个synchronized,代表着同步此方法,这样一来,一旦某个线程调用这个方法,实际上因为synchronized的原因此线程就获得了这个方法所在的对象的锁,其他线程若想再次调用此方法则必须排队等候,知道获得锁的线程执行完毕。

    6、volatile关键字

    首先要知道的就是volatile关键字的作用是什么?volatile的作用是使得变量在多个线程之间可见。

    这里通过一个例子做讲解,先创建一个线程

    class ThreadDemo extends Thread{
        boolean tag = true;
    
        public void run() {
    
            System.out.println("线程开始执行。。。");
            while (tag){
    
            }
            System.out.println("线程执行结束。。。");
        }
    
        public void isRun(boolean tag){
            this.tag = tag;
        }
    }

    通过一个while循环来代表线程一直执行,然后通过isRun方法来控制线程的结束,接下来在主线程中这样操作

    public class VolatileDemo {
    
    
        public static void main(String[] args) throws InterruptedException {
    
            ThreadDemo t1 = new ThreadDemo();
            t1.start();
            Thread.sleep(300);
            t1.isRun(false);
    
        }
    
    }

    可以想一下,线程会停止吗?实际运行的结果是不会,为什么呢?结合这张图来说明一下

    image

    这里的tag就是一个共享变量,首先子线程读取到的是在子线程中的本地内存中的共享变量副本,虽然在主线程中通过isRun方法将tag变成false,但是子线程中读取到的依然存放在本地内存中的副本依然是ture,也就是说通过isRun已经将主内存中的共享变量tag刷新成false,但是子线程并没有在主内存中读取这个刷新后的值,所以线程不会停止,那么如何解决这个问题呢?

    可以这样做

    image

    对tag使用volatile关键字,这样的话再次执行这个程序就会发现线程立马就结束了,这是因为一旦tag加上vola关键字,就强制要求每次使用tag都必须从主内存中取值,因此子线程可以拿到主内存中最新更新的tag也就是false,线程就自然而然的停止了。

    原因:
    线程之间是不可见的,读取的是副本,没有及时读取到主内存结果,解决办法是使用volatile关键字解决线程之间的可见性,强制线程每次读取该值的时候都去主内存中读取。

    7、线程之间的通信wait和notify(一)

    线程不是一个个独立的个体,线程与线程之间是可以进行互相通信和协作的。那么关于线程之间的通信,主要学习的就是等待和通知了,什么意思呢?也就是说在学习线程之间的通信这块,可能打交道最多的就是wait()和notify()这两个方法了。

    那么什么是wait和notify,从字面意思理解就是等待和通知的意思,那么在多线程中又是怎样的呢?

    在线程之间的通信中,wait就代表让此线程进入等待状态,之后的代码将不再执行,这里有一个前提就是,必须是在获得同步锁的前提下,Java为每一个Object对象都实现了wait和notify,但是不是随随便便就能调用的,比如这样

     public static void main(String[] args) {
            String s = new String();
            try {
                s.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    也就是说,在Java中任何一个对象都是可以调用这个wait和notify的,不过,能调用归能调用,出不出错就是另外一回事,例如上面这段代码,调用了wait这个方法,然后执行

    image

    结果出错,这是为什么?因为在使用wait和notify是有一个前提的,那就是事先必须已经获得了同步锁,以下才是正确的使用方式

       public static void main(String[] args) {
    
            try {
                String s = new String();
                synchronized (s){
                    s.wait();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    

    以上再次执行就不会报错了,因为已经加上了同步锁,这个只是为了说明对于wait和notify调用的前提是必须已经获得同步锁。

    也许还不是很清楚,那就记住一句话:只能在同步代码块和同步方法中调用wait和notify。

    接下来继续看一个例子

    public class Thread2 {
    
        public static void main(String[] args) {
            Object lock = new Object();
            MyThread myThread = new MyThread(lock);
            Thread thread = new Thread(myThread);
            thread.start();
            thread.setName("线程A");
    
        }
    }
    
    class MyThread implements Runnable{
    private Object lock;
    public MyThread(Object lock){
        this.lock = lock;
    }
        @Override
        public void run() {
            System.out.println("线程开始工作"+Thread.currentThread().getName());
            synchronized (lock){
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程结束工作"+Thread.currentThread().getName());
        }
    }

    首先从自定义的MyThread线程开始看,首先创建了一个object对象作为锁对象,然后在同步代码块中调用了wait方法,调用此方法的目的是让此线程处于等待状态,这样一来,之后的代码就不会再执行,同时调用此wait方法将线程置于等待状态的时候已经释放了持有的锁,先看一下以上代码的执行结果吧

    wait

    可以看得到,此时程序处于执行中状态,这是因为自定义的线程被处于等待队列的原因

    那么如何让这个线程继续执行剩下的代码呢?那就要使用notify这个方法了,再创建一个线程

    class OtherThread extends Thread{
        private Object lock;
        public OtherThread(Object lock){
            this.lock = lock;
        }
        @Override
        public void run() {
            System.out.println("线程开始工作"+Thread.currentThread().getName());
            synchronized (lock){
                lock.notify();
            }
            System.out.println("线程结束工作"+Thread.currentThread().getName());
        }
    }

    这里再同步代码块中则调用了notify来发起一个通知,发起一个什么通知呢?就是通知到那些调用了wait处于等待状态的线程,告诉他们你们可以执行啦,并且调用了notify的线程不会像调用了wait的线程那样立马释放掉锁,而是会将线程执行完毕才会释放锁,然后之前处于等待状态的线程拿到释放的锁继续执行剩下的代码,所以这里就有一个知识点,那就是这个锁必须是同一个锁,保证是同一个锁的关键点就是这些代码了

    private Object lock;
        public OtherThread(Object lock){
            this.lock = lock;
        }

    然后执行这个线程

      public static void main(String[] args) {
            //同一个锁
            Object lock = new Object();
    
            MyThread myThread = new MyThread(lock);
            Thread thread = new Thread(myThread);
            thread.start();
            thread.setName("线程A");
    
            OtherThread otherThread = new OtherThread(lock);
            otherThread.start();
            otherThread.setName("线程B");
    
        }

    紧接着执行程序

    image

    结果正如分析的一样!

    以上只是等待通知的一种情况,那就是一个线程处于等待状态,然后一个线程发起通知,紧接着处于等待的那个线程拿到发起通知的线程的锁,继而继续执行。

    当然,还有这么一种情况,那就是有很多个线程处于等待状态,然后一个线程发起通知,这样的话就会随机通知处于等待状态中的一个线程,其实还有一个方法叫做notifyAll是用来通知所有的等待线程的,这样的货,处于等待状态的这些个线程,谁的优先级高,谁就会得到这个通知,从而拿到锁。

    8、线程间通信join(二)

    首先来说这个jon有什么用,join是一个方法,线程可以调用这个方法,当在主线程中执行一个子线程,如果这个子线程执行结束会得到一个值,而在主线程中会用到这个值,但是实际的情况是,很有可能主线程已经执行完了,子线程还在执行,这样就无法得到这个值了,该怎么办

    使用join就可以解决这个问题,只需要让子线程调用join方法,这样就会在执行完子线程之后才会执行主线程,说简单点,就是子线程调用了join方法之后,就必须等子线程执行完成之后才能干其他的事

    使用起来也很简单

    thread.join();

    另外对于join还有这样的写法就是join(long),在代码中的表现形式就是如下

    join(2000);

    这样的话就会使得线程等待2秒之后执行,要知道单独使用这样的代码

    thread.join();

    是必须等到子线程结束之后才会执行其他的代码,但是如果是这样的话

    join(2000);

    两秒之后就会执行其它的代码了,看到这里,这个功能似乎跟这个有点像

    Thread.sleep(2000);

    但是两者有个本质上的区别就是join的话会释放锁,而sleep则不会,这个在具体的场景中则会有具体的应用。

    9、Lock的使用

    之前讲过使用synchronized关键字可以实现线程之间的同步,防止线程安全问题的产生,随着jdk版本的不断提升,在jdk1.5中出现了一个ReentrantLock也能实现相同的功能那个,而且比synchronized更加强大。

    使用ReentrantLock实现同步会更加的好理解,看代码

    class User{
        int age;
        public void setAge(int age0) {
            synchronized (this){
                this.age = age0;
                System.out.println("age="+age+"当前线程为:"+Thread.currentThread().getName());
            }
        }
    }

    之前是使用synchronized关键字来同步代码块保证线程之间的同步,以防止发生线程安全问题,那么该如何使用ReentrantLock来实现线程的同步呢

    class User{
        private Lock lock = new ReentrantLock();
        int age;
        public void setAge(int age0) {
            lock.lock();
                this.age = age0;
                System.out.println("age="+age+"当前线程为:"+Thread.currentThread().getName());
            lock.unlock();
        }
    }

    以上就是使用ReentrantLock来实现线程之间的同步了,可以看到,使用ReentrantLock是很好理解的,首先创建一个锁对象,然后上锁,执行相关代码,然后再释放锁。

    之前在讲synchronized的时候,可以通过wait和notify实现等待/通知模式,现在使用ReentrantLock同样可以完成等待通知模式,只不过这个要借助一个Condition类,具体的做法如下

    先看之前使用synchronized配合wait和notify实现的等待/通知模式

    public class Thread2 {
    
        public static void main(String[] args) {
            Object lock = new Object();
    
            MyThread myThread = new MyThread(lock);
            Thread thread = new Thread(myThread);
            thread.start();
            thread.setName("线程A");
    
    
    
            OtherThread otherThread = new OtherThread(lock);
            otherThread.start();
            otherThread.setName("线程B");
    
        }
    }
    
    class MyThread implements Runnable{
    private Object lock;
    public MyThread(Object lock){
        this.lock = lock;
    }
        @Override
        public void run() {
            System.out.println("线程开始工作"+Thread.currentThread().getName());
            synchronized (lock){
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程结束工作"+Thread.currentThread().getName());
        }
    }
    
    class OtherThread extends Thread{
        private Object lock;
        public OtherThread(Object lock){
            this.lock = lock;
        }
        @Override
        public void run() {
            System.out.println("线程开始工作"+Thread.currentThread().getName());
            synchronized (lock){
                lock.notify();
            }
            System.out.println("线程结束工作"+Thread.currentThread().getName());
        }
    }

    这里要把握一个重点就是在等待/通知模式中是需要共同的锁,所以在子线程中需要得到相同的锁,那么继续看使用ReentrantLock如何实现等待/通知模式,同样,把握一个重点,相同的锁,这里还需要一个相同的Condition对象

    首先创建两个线程,一个等待,一个通知

    class MyThead1 extends Thread{
        private Lock lock = new ReentrantLock();
        private Condition condition = lock.newCondition();
        public MyThead1(Lock lock , Condition condition){
            this.lock = lock;
            this.condition = condition;
        }
        @Override
        public void run() {
    
            try {
                lock.lock();
                System.out.println("线程开始工作"+Thread.currentThread().getName());
                condition.await();
                System.out.println("线程停止工作"+Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
    }
    class OtherThread1 extends Thread{
        private Lock lock = new ReentrantLock();
        private Condition condition = lock.newCondition();
        public OtherThread1(Lock lock , Condition condition){
            this.lock = lock;
            this.condition = condition;
        }
        @Override
        public void run() {
            lock.lock();
            System.out.println("线程开始执行"+Thread.currentThread().getName());
            condition.signal();
            System.out.println("线程停止执行"+Thread.currentThread().getName());
            lock.unlock();
        }
    }
    

    然后为了保证是同一把锁和Condition对象,这样操作

    private Lock lock = new ReentrantLock();
        private Condition condition = lock.newCondition();
        public MyThead1(Lock lock , Condition condition){
            this.lock = lock;
            this.condition = condition;
        }

    接下来在主程序中调用测试

     Lock lock = new ReentrantLock();
             Condition condition = lock.newCondition();
    
            MyThead1 myThead1 = new MyThead1(lock,condition);
            myThead1.start();
            myThead1.setName("线程A");

    看执行结果

    image

    果然,等待线程处于了等待状态,此时可以看到线程并未停止,而是处于运行状态,下面再调用通知线程查看结果

     Lock lock = new ReentrantLock();
             Condition condition = lock.newCondition();
    
            MyThead1 myThead1 = new MyThead1(lock,condition);
            myThead1.start();
            myThead1.setName("线程A");
    
            OtherThread1 otherThread1 = new OtherThread1(lock,condition);
            otherThread1.start();
            otherThread1.setName("线程B");

    看执行结果

    image

    可以看到,收到通知后,之前处于等待状态的线程继续执行了。

    在这里就需要知道这么几件事了

    1. 在等待/通知模式中,同一把锁很重要
    2. wait就相当于Condition中的await
    3. notify就相当于Condition中的signal
    4. notifyAll就相当于signalAll

    10、定时器Timer

    Java中多线程定时器的作用就是可以让一段代码持续性运行或者在规定的时间之后运行。

    先来看定时器的用法

    new Timer().schedule(new TimerTask() {
                @Override
                public void run() {
                    System.out.println("定时器一秒之后执行的");
                }
            },1000);

    以上代码可以直接放在主线程中运行,也就是一秒钟之后执行run中的代码,还可以这样操作

    看执行结果

    image

      new Timer().schedule(new TimerTask() {
                @Override
                public void run() {
                    System.out.println("定时器一秒之后执行的");
                }
            },1000,1000);

    这样的话就代表一秒钟之后执行run中的代码,然后每隔一秒重复执行一次,这样就实现循环。

    看执行代码

    image

    解释以上代码中的一些知识

    Timer类的主要作用就是设置计划任务,但封装任务的类却是TimerTask类,Timer实际上是一个工具类,然后可以调用schedule来执行相关的定时任务

    在第一个程序中实际上用到了这个
    schedule (TimerTask task, Date time)

    可以看到,对于这个schedule需要传入一个TimerTask,还需要一个时间,这里的意思就是经过多少时间之后执行这个任务。

    对于第二个程序是这样的schedule(TimerTack task, Date firstTime, long period)

    相比于第一个在schedule中又多传入一个值,这个值就代表经过这个时间之后再次执行这个任务,相当于无限循环执行。

    实际的效果自己测试一下就会明了。

  • 相关阅读:
    microsoft visual studio 不能逐句执行?
    【转】字符编码笔记:ASCII,Unicode和UTF-8
    【PNG格式中文详解】
    PHP 下载网络图片
    Install MongoDB on Windows (Windows下安装MongoDB)
    S2SH商用后台权限系统第二讲
    S2SH商用后台权限系统第一讲
    linux 常用命令
    简单的angular表单验证指令
    angular随笔
  • 原文地址:https://www.cnblogs.com/ithuangqing/p/12113595.html
Copyright © 2020-2023  润新知