美文网首页
Java高并发系列——检视阅读(二)

Java高并发系列——检视阅读(二)

作者: 卡斯特梅的雨伞 | 来源:发表于2020-09-14 11:21 被阅读0次

    Java高并发系列——线程的基本操作

    线程的基本操作

    新建线程

    start方法是启动一个线程,run方法只会在当前线程中串行的执行run方法中的代码。

    我们可以通过继承Thread类,然后重写run方法,来自定义一个线程。但考虑java是单继承的,从扩展性上来说,我们实现一个接口来自定义一个线程更好一些,java中刚好提供了Runnable接口来自定义一个线程。实现Runnable接口是比较常见的做法,也是推荐的做法。

    终止线程——stop()方法已废弃

    stop方法为何会被废弃而不推荐使用?stop方法过于暴力,强制把正在执行的方法停止了。

    大家是否遇到过这样的场景:听着歌写着代码突然断电了。线程正在运行过程中,被强制结束了,可能会导致一些意想不到的后果。可以给大家发送一个通知,告诉大家保存一下手头的工作,将电脑关闭

    线程中断——interrupt()正确的中断线程方法

    线程中断并不会使线程立即退出,而是给线程发送一个通知,告知目标线程,有人希望你退出了!至于目标线程接收到通知之后如何处理,则完全由目标线程自己决定,这点很重要,如果中断后,线程立即无条件退出,我们又会到stop方法的老问题。

    Thread提供了3个与线程中断有关的方法,这3个方法容易混淆,大家注意下:

    public void interrupt() //中断线程
    public boolean isInterrupted() //判断线程是否被中断
    public static boolean interrupted()  //判断线程是否被中断,并清除当前中断状态
    

    interrupt()方法是一个实例方法,它通知目标线程中断,也就是设置中断标志位为true,中断标志位表示当前线程已经被中断了。

    isInterrupted()方法也是一个实例方法,它判断当前线程是否被中断(通过检查中断标志位)。

    interrupted()是一个静态方法,返回boolean类型,也是用来判断当前线程是否被中断,但是同时会清除当前线程的中断标志位的状态。

    Q:通过变量控制和线程自带的interrupt方法来中断线程有什么区别呢?

    A:如果一个线程调用了sleep方法,一直处于休眠状态,通过变量控制,是不能中断线程么,因为此时线程处于睡眠状态无法执行变量控制语句,此时只能使用线程提供的interrupt方法来中断线程了。

    实例:

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                while (true) {
                    try {
                        TimeUnit.SECONDS.sleep(20);
                    } catch (InterruptedException e) {
                        //sleep方法由于中断而抛出异常之后,线程的中断标志会被清除(置为false),所以在异常中需要执行this.interrupt()方法,将中断标志位置为true
                        this.interrupt();
                        System.out.println("exception:"+ e.getMessage());
                    }
                    System.out.println(Thread.currentThread().getName() + " in the end");
                    break;
                }
    
            }
        };
        t1.setName("interrupt thread");
        t1.start();
        TimeUnit.SECONDS.sleep(1);
        //调用interrupt()方法之后,线程的sleep方法将会抛出 InterruptedException: sleep interrupted异常。
        t1.interrupt();
    }
    

    错误写法:

    image.png

    注意:sleep方法由于中断而抛出异常之后,线程的中断标志会被清除(置为false),所以在异常中需要执行this.interrupt()方法,将中断标志位置为true

    等待(wait)和通知(notify)

    为了支持多线程之间的协作,JDK提供了两个非常重要的方法:等待wait()方法和通知notify()方法。这2个方法并不是在Thread类中的,而是在Object类中定义的这意味着所有的对象都可以调用者两个方法

    (即只有这个对象是被当成锁来作为多线程之间的协作对象,那么在同步代码块中,线程之间就是通过等待wait()方法和通知notify()方法协作。)

    public final void wait() throws InterruptedException;
    public final native void notify();
    

    如果一个线程调用了object.wait()方法,那么它就会进出object对象的等待队列。这个队列中,可能会有多个线程,因为系统可能运行多个线程同时等待某一个对象。当object.notify()方法被调用时,它就会从这个队列中随机选择一个线程,并将其唤醒。这里希望大家注意一下,这个选择是不公平的,并不是先等待线程就会优先被选择,这个选择完全是随机的。 nofiyAll()方法,它和notify()方法的功能类似,不同的是,它会唤醒在这个等待队列中所有等待的线程,而不是随机选择一个。

    这里强调一点,Object.wait()方法并不能随便调用。它必须包含在对应的synchronize语句块中,无论是wait()方法或者notify()方法都需要首先获取目标独享的一个监视器

    等待wait()方法和通知notify()方法工作过程:

    image.png

    wait()方法和nofiy()方法的工作流程细节:

    image.png

    图中其中T1和T2表示两个线程。T1在正确执行wait()方法前,必须获得object对象的监视器。而wait()方法在执行后,会释放这个监视器。这样做的目的是使其他等待在object对象上的线程不至于因为T1的休眠而全部无法正常执行。

    线程T2在notify()方法调用前,也必须获得object对象的监视器。所幸,此时T1已经释放了这个监视器,因此,T2可以顺利获得object对象的监视器。接着,T2执行了notify()方法尝试唤醒一个等待线程,这里假设唤醒了T1。T1在被唤醒后,要做的第一件事并不是执行后续代码,而是要尝试重新获得object对象的监视器,而这个监视器也正是T1在wait()方法执行前所持有的那个。如果暂时无法获得,则T1还必须等待这个监视器。当监视器顺利获得后,T1才可以在真正意义上继续执行。

    注意:Object.wait()方法和Thread.sleeep()方法都可以让现场等待若干时间。除wait()方法可以被唤醒外,另外一个主要的区别就是wait()方法会释放目标对象的锁,而Thread.sleep()方法不会释放锁。

    示例:

    public class WaitNotifyTest {
        public static Object lock = new Object();
    
        public static void main(String[] args) {
            new T1("Thread-1").start();
            new T2("Thread-2").start();
        }
    
        static class T1 extends Thread {
            public T1(String name) {
                super(name);
            }
    
            @Override
            public void run() {
                synchronized (lock) {
                    System.out.println(this.getName() + " start");
                    try {
                        System.out.println(this.getName() + " wait");
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(this.getName() + " end");
                }
            }
        }
    
        static class T2 extends Thread {
            public T2(String name) {
                super(name);
            }
    
            @Override
            public void run() {
                synchronized (lock) {
                    System.out.println(this.getName() + " start");
                    System.out.println(this.getName() + " notify");
                    lock.notify();
                    System.out.println(this.getName() + " end");
                    try {
                        TimeUnit.SECONDS.sleep(2);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(this.getName() + " end,2 second later");
                }
            }
        }
    
    }
    

    输出:

    Thread-1 start
    Thread-1 wait
    Thread-2 start
    Thread-2 notify
    Thread-2 end
    Thread-2 end,2 second later
    Thread-1 end
    注意下打印结果,T2调用notify方法之后,T1并不能立即继续执行,而是要等待T2释放objec投递锁之后,T1重新成功获取锁后,才能继续执行。因此最后2行日志相差了2秒(因为T2调用notify方法后休眠了2秒)。
    

    可以这么理解,obj对象上有2个队列,q1:等待队列,q2:准备获取锁的队列

    挂起(suspend)和继续执行(resume)线程——方法已废弃

    Thread类中还有2个方法,即线程挂起(suspend)继续执行(resume),这2个操作是一对相反的操作,被挂起的线程,必须要等到resume()方法操作后,才能继续执行。系统中已经标注着2个方法过时了,不推荐使用

    系统不推荐使用suspend()方法去挂起线程是因为suspend()方法导致线程暂停的同时,并不会释放任何锁资源。此时,其他任何线程想要访问被它占用的锁时,都会被牵连,导致无法正常运行(如图2.7所示)。直到在对应的线程上进行了resume()方法操作,被挂起的线程才能继续,从而其他所有阻塞在相关锁上的线程也可以继续执行。但是,如果resume()方法操作意外地在suspend()方法前就被执行了,那么被挂起的线程可能很难有机会被继续执行了。并且,更严重的是:它所占用的锁不会被释放,因此可能会导致整个系统工作不正常。而且,对于被挂起的线程,从它线程的状态上看,居然还是Runnable状态,这也会影响我们对系统当前状态的判断。

    image.png

    等待线程结束(join)和谦让(yeild)

    很多时候,一个线程的输入可能非常依赖于另外一个或者多个线程的输出,此时,这个线程就需要等待依赖的线程执行完毕,才能继续执行。jdk提供了join()操作来实现等待线程结束

    //表示无限等待,当前线程会一直等待,直到目标线程执行完毕
    public final void join() throws InterruptedException;
    //millis参数用于指定等待时间,如果超过了给定的时间目标线程还在执行,当前线程也会停止等待,而继续往下执行。
    public final synchronized void join(long millis) throws InterruptedException;
    

    例子:线程T1需要等待T2、T3完成之后才能继续执行,那么在T1线程中需要分别调用T2和T3的join()方法。

    示例:

    public class JoinTest {
        private static int num = 0;
    
        public static void main(String[] args) throws InterruptedException {
            T1 t1 = new T1("T1");
            t1.start();
            long start = System.currentTimeMillis();
            t1.join();
            long end = System.currentTimeMillis();
            System.out.println(Thread.currentThread().getName() + " end .user time ="+(end-start)+" ,get num=" + num);
        }
    
        static class T1 extends Thread {
            public T1(String name) {
                super(name);
            }
    
            @Override
            public void run() {
                System.out.println(this.getName() + " start");
                for (int i = 0; i < 9; i++) {
                    num++;
                }
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(this.getName() + " end");
            }
        }
    }
    

    输出:

    T1 start
    T1 end
    main end .user time =3003 ,get num=9
    

    Thread.yield()方法。是屈服,放弃,谦让的意思。

    这是一个静态方法,一旦执行,它会让当前线程让出CPU,但需要注意的是,让出CPU并不是说不让当前线程执行了,当前线程在出让CPU后,还会进行CPU资源的争夺,但是能否再抢到CPU的执行权就不一定了。因此,对Thread.yield()方法的调用好像就是在说:我已经完成了一些主要的工作,我可以休息一下了,可以让CPU给其他线程一些工作机会了。

    如果觉得一个线程不太重要,或者优先级比较低,而又担心此线程会过多的占用CPU资源,那么可以在适当的时候调用一下Thread.yield()方法,给与其他线程更多的机会

    public static native void yield();
    

    总结

    1. 创建线程的4种方式:继承Thread类;实现Runnable接口;实现Callable接口;使用线程池创建。
    2. 启动线程:调用线程的start()方法
    3. 终止线程:调用线程的stop()方法,方法已过时,建议不要使用
    4. 线程中断相关的方法:调用线程实例interrupt()方法将中断标志置为true;使用线程实例方法isInterrupted()获取中断标志;调用Thread的静态方法interrupted()获取线程是否被中断,此方法调用之后会清除中断标志(将中断标志置为false了)
    5. wait、notify、notifyAll方法
    6. 线程挂起使用线程实例方法suspend(),恢复线程使用线程实例方法resume(),这2个方法都过时了,不建议使用
    7. 等待线程结束:调用线程实例方法join()
    8. 让出cpu资源:调用线程静态方法yeild()
    疑问:

    Q:方法interrupted()是一个静态方法,返回boolean类型,也是用来判断当前线程是否被中断,但是同时会清除当前线程的中断标志位的状态。 清除当前线程的中断标志位的状态是表示该线程可以不中断了么?清除当前线程的中断标志位的状态是什么意思,有什么作用?怎么使用?

    Q:三个线程交替打印ABC 10次使用wait(),notifyAll()如何实现?

    public class AlternatePrint {
        public static Object lock = new Object();
        public static AtomicInteger count = new AtomicInteger(0);
    
        public static void main(String[] args) {
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    synchronized (lock) {
                        while (count.get() % 3 != 0) {
                            try {
                                lock.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        System.out.print(Thread.currentThread().getName());
                        count.incrementAndGet();
                        lock.notifyAll();
                    }
                }
            }, "A").start();
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    synchronized (lock) {
                        while (count.get() % 3 != 1) {
                            try {
                                lock.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        System.out.print(Thread.currentThread().getName());
                        count.incrementAndGet();
                        lock.notifyAll();
                    }
                }
            }, "B").start();
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    synchronized (lock) {
                        while (count.get() % 3 != 2) {
                            try {
                                lock.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        System.out.print(Thread.currentThread().getName());
                        count.incrementAndGet();
                        lock.notifyAll();
                    }
                }
            }, "C").start();
        }
    }
    

    LockSupport实现三个线程交替打印ABC 10次

    public class AlternatePrint2 {
        public static Thread a, b, c;
    
        public static void main(String[] args) {
            a = new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    LockSupport.park();
                    System.out.print(Thread.currentThread().getName());
                    LockSupport.unpark(b);
    
                }
            }, "A");
            b = new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    LockSupport.park();
                    System.out.print(Thread.currentThread().getName());
                    LockSupport.unpark(c);
                }
            }, "B");
            c = new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    LockSupport.park();
                    System.out.print(Thread.currentThread().getName());
                    LockSupport.unpark(a);
                }
            }, "C");
            a.start();
            b.start();
            c.start();
            //注意进入后先都让线程阻塞住,否则会被其他线程率先执行导致失序。
            LockSupport.unpark(a);
        }
    }
    

    volatile与Java内存模型

    volatile解决了共享变量在多线程中可见性的问题,可见性是指一个线程对共享变量的修改,对于另一个线程来说是否是可以看到的。

    使用volatile保证内存可见性示例:

    public class VolatileTest {
    
        //public static boolean flag = true;
        public static volatile boolean flag = true;
    
        public static void main(String[] args) throws InterruptedException {
            T1 t1 = new T1("T1");
            t1.start();
            //TimeUnit.SECONDS.sleep(3);
            Thread.sleep(2000);
            //将flag置为false
            flag = false;
        }
    
        public static class T1 extends Thread {
            public T1(String name) {
                super(name);
            }
    
            @Override
            public void run() {
                System.out.println(this.getName() + " start");
                while (VolatileTest.flag) {
                    //奇怪现象,为什么执行输出语句在运行一会儿后会让flag=false读取到,而 ; 空循环却会导致程序无法终止呢?
                    //个人觉得应该是虚拟机从解释执行转换为编译执行,这时候会重新读到flag。
                    //System.out.println(this.getName() +"endless loop");
                    ;
                }
                System.out.println(this.getName() + " end");
            }
        }
    }
    

    不加volatile运行上面代码,会发现程序无法终止。

    Q:t1线程中为什么看不到被主线程修改之后的flag?

    要解释这个,我们需要先了解一下java内存模型(JMM),Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。

    Q: 上面的代码为什么执行输出语句在运行一会儿后会让flag=false读取到,而 ; 空循环却会导致程序无法终止呢?

    RednaxelaFX的回答:JIT编译器的优化

    image.png

    Java内存模型的抽象示意图:

    image.png

    线程A需要和线程B通信,必须要经历下面2个步骤:

    1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
    2. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量

    线程t1中为何看不到被主线程修改为false的flag的值的原因,有两种可能:

    1. 主线程修改了flag之后,未将其刷新到主内存,所以t1看不到
    2. 主线程将flag刷新到了主内存,但是t1一直读取的是自己工作内存中flag的值,没有去主内存中获取flag最新的值

    使用volatile修饰共享变量,就可以达到上面的效果,被volatile修改的变量有以下特点

    1. 线程中读取的时候,每次读取都会去主内存中读取共享变量最新的值,然后将其复制到工作内存(正确的来说是当主内存有变化时,各个工作内存的副本会被缓存一致性协议和总线嗅探机制作用而失效,因此当其他线程去获取工作内存上的值时发现失效了或重新获取最新的主内存上的值)
    2. 线程中修改了工作内存中变量的副本,修改之后会立即刷新到主内存

    线程组

    我们可以把线程归属到某个线程组中,线程组可以包含多个线程以及线程组,线程和线程组组成了父子关系,是个树形结构。使用线程组可以方便管理线程 。(线程池是不是更实在一点?)

    image.png

    创建线程关联线程组

    创建线程的时候,可以给线程指定一个线程组。

    创建线程组的时候,可以给其指定一个父线程组,也可以不指定,如果不指定父线程组,则父线程组为当前线程的线程组,系统自动获取当前线程的线程组作为默认父线程组。java api有2个常用的构造方法用来创建线程组:

    public ThreadGroup(String name) public ThreadGroup(ThreadGroup parent, String name)
    

    第一个构造方法未指定父线程组,看一下内部的实现:

    public ThreadGroup(String name) {        this(Thread.currentThread().getThreadGroup(), name);    }
    

    批量停止线程

    调用线程组interrupt(),会将线程组树下的所有子孙线程中断标志置为true,可以用来批量中断线程。

    建议创建线程或者线程组的时候,给他们取一个有意义的名字,在系统出问题的时候方面查询定位。

    示例:

    public class ThreadGroupTest {
        public static class R1 implements Runnable {
            @Override
            public void run() {
                System.out.println("threadName:" + Thread.currentThread().getName());
                while (!Thread.currentThread().isInterrupted()){
                    ;
                }
                System.out.println(Thread.currentThread().getName()+"线程停止了");
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            //threadGroup1未指定父线程组,系统获取了主线程的线程组作为threadGroup1的父线程组,输出结果中是:main
            ThreadGroup threadGroup = new ThreadGroup("thread-group-1");
            Thread t1 = new Thread(threadGroup, new R1(), "t1");
            Thread t2 = new Thread(threadGroup, new R1(), "t2");
            t1.start();
            t2.start();
            TimeUnit.SECONDS.sleep(1);
            System.out.println("活动线程数:" + threadGroup.activeCount());
            System.out.println("活动线程组:" + threadGroup.activeGroupCount());
            System.out.println("线程组名称:" + threadGroup.getName());
            ThreadGroup threadGroup2 = new ThreadGroup(threadGroup, "thread-group-2");
            Thread t3 = new Thread(threadGroup2, new R1(), "t3");
            Thread t4 = new Thread(threadGroup2, new R1(), "t4");
            t3.start();
            t4.start();
            threadGroup.list();
            //java.lang.ThreadGroup[name=main,maxpri=10] 主线程的线程组为main
            System.out.println(Thread.currentThread().getThreadGroup());
            //java.lang.ThreadGroup[name=system,maxpri=10] 根线程组为system
            System.out.println(Thread.currentThread().getThreadGroup().getParent());
            //null
            System.out.println(Thread.currentThread().getThreadGroup().getParent().getParent());
    
            threadGroup.interrupt();
            TimeUnit.SECONDS.sleep(2);
            threadGroup.list();
        }
    }
    

    输出:

    threadName:t1
    threadName:t2
    活动线程数:2
    活动线程组:0
    线程组名称:thread-group-1
    java.lang.ThreadGroup[name=thread-group-1,maxpri=10]
        Thread[t1,5,thread-group-1]
        Thread[t2,5,thread-group-1]
        java.lang.ThreadGroup[name=thread-group-2,maxpri=10]
            Thread[t3,5,thread-group-2]
            Thread[t4,5,thread-group-2]
    java.lang.ThreadGroup[name=main,maxpri=10]
    java.lang.ThreadGroup[name=system,maxpri=10]
    null
    t2线程停止了
    t1线程停止了
    threadName:t4
    threadName:t3
    t4线程停止了
    t3线程停止了
    java.lang.ThreadGroup[name=thread-group-1,maxpri=10]
        java.lang.ThreadGroup[name=thread-group-2,maxpri=10]
    

    用户线程和守护线程

    守护线程是一种特殊的线程,在后台默默地完成一些系统性的服务,比如垃圾回收线程JIT线程都是守护线程。与之对应的是用户线程,用户线程可以理解为是系统的工作线程,它会完成这个程序需要完成的业务操作。如果用户线程全部结束了,意味着程序需要完成的业务操作已经结束了,系统可以退出了。所以当系统只剩下守护进程的时候,java虚拟机会自动退出

    java线程分为用户线程和守护线程,线程的daemon属性为true表示是守护线程,false表示是用户线程。

    线程daemon的默认值

    我们看一下创建线程源码,位于Thread类的init()方法中:

    Thread parent = currentThread();
    this.daemon = parent.isDaemon();
    

    dameon的默认值为为父线程的daemon,也就是说,父线程如果为用户线程,子线程默认也是用户线程,父线程如果是守护线程,子线程默认也是守护线程。

    总结

    1. java中的线程分为用户线程守护线程
    2. 程序中的所有的用户线程结束之后,不管守护线程处于什么状态,java虚拟机都会自动退出
    3. 调用线程的实例方法setDaemon()来设置线程是否是守护线程
    4. setDaemon()方法必须在线程的start()方法之前调用,在后面调用会报异常,并且不起效
    5. 线程的daemon默认值和其父线程一样

    示例:

    public class DaemonThreadTest {
    
        public static class T1 extends Thread {
            public T1(String name) {
                super(name);
            }
    
            @Override
            public void run() {
                System.out.println(this.getName() + " start ,isDaemon= "+isDaemon());
                while (true) {
                    ;
                }
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            T1 t1 = new T1("T1");
            // 设置守护线程,需要在start()方法之前进行
            // t1.start()必须在setDaemon(true)之后,否则执行会报异常:Exception in thread "main" java.lang.IllegalThreadStateException
            //t1.start();
            //将t1线程设置为守护线程
            t1.setDaemon(true);
            t1.start();
            //当程序中所有的用户线程执行完毕之后,不管守护线程是否结束,系统都会自动退出。
            TimeUnit.SECONDS.sleep(1);
        }
    }
    
    疑问:

    Q:JIT线程?

    A: JIT一般指准时制。准时制生产方式(Just In Time简称JIT ).JIT线程在Java中表示即时编译线程。

    在Java编程语言和环境中,即时编译器(JIT compiler,just-in-time compiler)是一个把Java的字节码(包括需要被解释的指令的程序)转换成可以直接发送给处理器的指令的程序。

    线程安全和synchronized关键字

    什么是线程安全?

    当多个线程去访问同一个类(对象或方法)的时候,该类都能表现出一致的行为,没有意想不到的不同结果,那我们就可以说这个类是线程安全的。

    造成线程安全问题的主要诱因有两点:

    1. 一是存在共享数据(也称临界资源)
    2. 二是存在多条线程共同操作共享数据

    为了解决这个问题,当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据,这种方式有个高尚的名称叫互斥锁,在 Java 中,关键字 synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作)同时我们还应该注意到synchronized另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代volatile功能)

    锁的互斥性表现在线程尝试获取的是否是同一个锁,相同类型不同实例的对象锁不互斥,而class类对象的锁与实例锁之间也不互斥。

    synchronized主要有3种使用方式

    1. 修饰实例方法,作用于当前实例,进入同步代码前需要先获取实例的锁
    2. 修饰静态方法,作用于类的Class对象,进入修饰的静态方法前需要先获取类的Class对象的锁
    3. 修饰代码块,需要指定加锁对象(记做lockobj),在进入同步代码块前需要先获取lockobj的锁

    synchronized作用于实例对象

    synchronize作用于实例方法需要注意:

    1. 实例方法上加synchronized,线程安全的前提是,多个线程操作的是同一个实例,如果多个线程作用于不同的实例,那么线程安全是无法保证的
    2. 同一个实例的多个实例方法上有synchronized,这些方法都是互斥的,同一时间只允许一个线程操作同一个实例的其中的一个synchronized方法

    synchronized作用于静态方法

    当synchronized作用于静态方法时,锁的对象就是当前类的Class对象。

    synchronized同步代码块

    方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分时使用。加锁时可以使用自定义的对象作为锁,也可以使用this对象(代表当前实例)或者当前类的class对象作为锁 。

    疑问:

    Q:synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代volatile功能),synchronized是怎么保证可见性的呢?

    Java原子操作要求lock和unlock操作时必须做到可见性,即对主内存的副本工作内存做修改时会马上回写主内存;还有就是读取工作内存副本数据时每次都会读主内存最新的数据。

    Q:同一个实例的多个实例方法上有synchronized,这些方法都是互斥的,同一时间只允许一个线程操作同一个实例的其中的一个synchronized方法.验证同一时间只允许一个线程操作同一个实例的其中的一个synchronized方法是对的。

    A:示例有下:

    public class MethodObject {
    
        public synchronized void methodA() throws InterruptedException {
            System.out.println("methodA start");
            TimeUnit.SECONDS.sleep(10);
            System.out.println("methodA finish");
        }
    
        public synchronized void methodB() throws InterruptedException {
            System.out.println("methodB start");
            TimeUnit.SECONDS.sleep(5);
            System.out.println("methodB finish");
        }
    
    }
    
    public class SynchronousTest {
    
        public static void main(String[] args) throws InterruptedException {
            MethodObject mo = new MethodObject();
            T1 t1 = new T1("T1", mo);
            T2 t2 = new T2("T2", mo);
            t1.start();
            TimeUnit.MILLISECONDS.sleep(300);
            t2.start();
        }
    
        public static class T1 extends Thread {
            private  MethodObject mo;
            public T1(String name, MethodObject mo) {
                super(name);
                this.mo = mo;
            }
    
            @Override
            public void run() {
                try {
                    mo.methodA();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        public static class T2 extends Thread {
            private  MethodObject mo;
            public T2(String name, MethodObject mo) {
                super(name);
                this.mo = mo;
            }
    
            @Override
            public void run() {
                try {
                    mo.methodB();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

    synchronized实现原理

    深入理解Java并发之synchronized实现原理

    线程中断的2种方式

    1、通过一个volatile修饰的变量控制线程中断

    利用volatile控制的变量在多线程中的可见性,Java内存模型实现。

    示例:

    public class VolatileTest {
    
        //public static boolean flag = true;
        public static volatile boolean flag = true;
    
        public static void main(String[] args) throws InterruptedException {
            T1 t1 = new T1("T1");
            t1.start();
            //TimeUnit.SECONDS.sleep(3);
            Thread.sleep(3000);
            //将flag置为false
            flag = false;
        }
    
        public static class T1 extends Thread {
            public T1(String name) {
                super(name);
            }
    
            @Override
            public void run() {
                System.out.println(this.getName() + " start");
                while (VolatileTest.flag) {
                    ;
                }
                System.out.println(this.getName() + " end");
            }
        }
    }
    

    2、通过线程自带的中断标志interrupt() 控制

    当调用线程的interrupt()实例方法之后,线程的中断标志会被置为true,可以通过线程的实例方法isInterrupted()获取线程的中断标志。

    当运行的线程处于阻塞状态时:

    1. 调用线程的interrupt()实例方法,线程的中断标志会被置为true
    2. 当线程处于阻塞状态时,调用线程的interrupt()实例方法,线程内部会触发InterruptedException异常,并且会清除线程内部的中断标志(即将中断标志置为false)

    阻塞状态处理方法:这时候应该在catch中再调用this.interrupt();一次,将中断标志置为true。然后在run()方法中通过this.isInterrupted()来获取线程的中断标志,退出循环break。

    总结

    1. 当一个线程处于被阻塞状态或者试图执行一个阻塞操作时,可以使用 Thread.interrupt()方式中断该线程,注意此时将会抛出一个InterruptedException的异常,同时中断状态将会被复位(由中断状态改为非中断状态)。阻塞状态线程要通过线程自带的中断标志interrupt() 控制中断。
    2. 内部有循环体,可以通过一个变量来作为一个信号控制线程是否中断,注意变量需要volatile修饰。
    3. 文中的2种方式可以结合起来灵活使用控制线程的中断.

    示例:

    public class InterruptTest1 {
    
        public static class T1 extends Thread {
            public T1(String name) {
                super(name);
            }
    
            @Override
            public void run() {
                System.out.println(this.getName() + " start");
                while (true) {
                    try {
                        //下面模拟阻塞代码
                        TimeUnit.SECONDS.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        this.interrupt();
                    }
                    if (this.isInterrupted()) {
                        break;
                    }
                }
                System.out.println(this.getName() + " end");
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            T1 t1 = new T1("thread1");
            t1.start();
            TimeUnit.SECONDS.sleep(2);
            t1.interrupt();
        }
    }
    

    输出:

    thread1 start
    thread1 end
    java.lang.InterruptedException: sleep interrupted
        at java.lang.Thread.sleep(Native Method)
        at java.lang.Thread.sleep(Thread.java:340)
        at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
        at com.self.current.InterruptTest1$T1.run(InterruptTest1.java:27)
    

    相关文章

      网友评论

          本文标题:Java高并发系列——检视阅读(二)

          本文链接:https://www.haomeiwen.com/subject/hjdbektx.html