java工具-高并发-线程常用技术

volatile与Java内存模型

java 复制代码
public class Demo09 {

    public static boolean flag = true;




    public static class T1 extends Thread {

        public T1(String name) {

            super(name);

        }




        @Override

        public void run() {

            System.out.println("线程" + this.getName() + " in");

            while (flag) {

                ;

            }

            System.out.println("线程" + this.getName() + "停止了");

        }

    }




    public static void main(String[] args) throws InterruptedException {

        new T1("t1").start();

        //休眠1秒

        Thread.sleep(1000);

        //将flag置为false

        flag = false;

    }

}

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

线程t1的run()方法中有个循环,通过flag来控制循环是否结束,主线程中休眠了1秒,将flag置为false,按说此时线程t1会检测到flag为false,打印"线程t1停止了",为何和我们期望的结果不一样呢?运行上面的代码我们可以判断,t1中看到的flag一直为true,主线程将flag置为false之后,t1线程中并没有看到,所以一直死循环。

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

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

从上图中可以看出,线程A需要和线程B通信,必须要经历下面2个步骤:

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

下面通过示意图来说明这两个步骤:

如上图所示,本地内存A和B有主内存中共享变量x的副本。假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。

对JMM了解之后,我们再看看文章开头的问题,线程t1中为何看不到被主线程修改为false的flag的值,有两种可能:

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

对于上面2种情况,有没有什么办法可以解决?

是否有这样的方法:线程中修改了工作内存中的副本之后,立即将其刷新到主内存;工作内存中每次读取共享变量时,都去主内存中重新读取,然后拷贝到工作内存。

java帮我们提供了这样的方法,使用volatile修饰共享变量,就可以达到上面的效果,被volatile修改的变量有以下特点:

  1. 线程中读取的时候,每次读取都会去主内存中读取共享变量最新的值,然后将其复制到工作内存
  2. 线程中修改了工作内存中变量的副本,修改之后会立即刷新到主内存

我们修改一下开头的示例代码:

java 复制代码
public volatile static boolean flag = true;

使用volatile修饰flag变量,然后运行一下程序,输出:

线程t1 in

线程t1停止了

这下程序可以正常停止了。

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

线程组

我们可以把线程归属到某个线程组中,线程组可以包含多个线程 以及线程组,线程和线程组组成了父子关系,是个树形结构,如下图:

使用线程组可以方便管理线程,线程组提供了一些方法方便方便我们管理线程。

创建线程关联线程组

创建线程的时候,可以给线程指定一个线程组,代码如下:

java 复制代码
import java.util.concurrent.TimeUnit;


public class Demo1 {

    public static class R1 implements Runnable {

        @Override

        public void run() {

            System.out.println("threadName:" + Thread.currentThread().getName());

            try {

                TimeUnit.SECONDS.sleep(3);

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

        }

    }




    public static void main(String[] args) throws InterruptedException {

        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());

    }

}

输出结果:

java 复制代码
threadName:t1

threadName:t2

活动线程数:2

活动线程组:0

线程组名称:thread-group-1

activeCount() 方法可以返回线程组中的所有活动线程数,包含下面的所有子孙节点的线程,由于线程组中的线程是动态变化的,这个值只能是一个估算值。

为线程组指定父线程组

创建线程组的时候,可以给其指定一个父线程组,也可以不指定,如果不指定父线程组,则父线程组为当前线程的线程组,java api有2个常用的构造方法用来创建线程组:

java 复制代码
public ThreadGroup(String name) 

public ThreadGroup(ThreadGroup parent, String name)

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

java 复制代码
public ThreadGroup(String name) {

        this(Thread.currentThread().getThreadGroup(), name);

    }

系统自动获取当前线程的线程组作为默认父线程组。

上一段示例代码:

java 复制代码
import java.util.concurrent.TimeUnit;


public class Demo2 {

    public static class R1 implements Runnable {

        @Override

        public void run() {

            Thread thread = Thread.currentThread();

            System.out.println("所属线程组:" + thread.getThreadGroup().getName() + ",线程名称:" + thread.getName());

            try {

                TimeUnit.SECONDS.sleep(3);

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

        }

    }




    public static void main(String[] args) throws InterruptedException {

        ThreadGroup threadGroup1 = new ThreadGroup("thread-group-1");

        Thread t1 = new Thread(threadGroup1, new R1(), "t1");

        Thread t2 = new Thread(threadGroup1, new R1(), "t2");

        t1.start();

        t2.start();

        TimeUnit.SECONDS.sleep(1);

        System.out.println("threadGroup1活动线程数:" + threadGroup1.activeCount());

        System.out.println("threadGroup1活动线程组:" + threadGroup1.activeGroupCount());

        System.out.println("threadGroup1线程组名称:" + threadGroup1.getName());

        System.out.println("threadGroup1父线程组名称:" + threadGroup1.getParent().getName());

        System.out.println("----------------------");

        ThreadGroup threadGroup2 = new ThreadGroup(threadGroup1, "thread-group-2");

        Thread t3 = new Thread(threadGroup2, new R1(), "t3");

        Thread t4 = new Thread(threadGroup2, new R1(), "t4");

        t3.start();

        t4.start();

        TimeUnit.SECONDS.sleep(1);

        System.out.println("threadGroup2活动线程数:" + threadGroup2.activeCount());

        System.out.println("threadGroup2活动线程组:" + threadGroup2.activeGroupCount());

        System.out.println("threadGroup2线程组名称:" + threadGroup2.getName());

        System.out.println("threadGroup2父线程组名称:" + threadGroup2.getParent().getName());




        System.out.println("----------------------");

        System.out.println("threadGroup1活动线程数:" + threadGroup1.activeCount());

        System.out.println("threadGroup1活动线程组:" + threadGroup1.activeGroupCount());




        System.out.println("----------------------");

        threadGroup1.list();

    }

}

输出结果:

java 复制代码
所属线程组:thread-group-1,线程名称:t1

所属线程组:thread-group-1,线程名称:t2

threadGroup1活动线程数:2

threadGroup1活动线程组:0

threadGroup1线程组名称:thread-group-1

threadGroup1父线程组名称:main

----------------------

所属线程组:thread-group-2,线程名称:t4

所属线程组:thread-group-2,线程名称:t3

threadGroup2活动线程数:2

threadGroup2活动线程组:0

threadGroup2线程组名称:thread-group-2

threadGroup2父线程组名称:thread-group-1

----------------------

threadGroup1活动线程数:4

threadGroup1活动线程组: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]

代码解释:

  1. threadGroup1未指定父线程组,系统获取了主线程的线程组作为threadGroup1的父线程组,输出结果中是:main
  2. threadGroup1为threadGroup2的父线程组
  3. threadGroup1活动线程数为4,包含了threadGroup1线程组中的t1、t2,以及子线程组threadGroup2中的t3、t4
  4. 线程组的list()方法,将线程组中的所有子孙节点信息输出到控制台,用于调试使用

根线程组

获取根线程组

java 复制代码
public class Demo3 {


    public static void main(String[] args) {

        System.out.println(Thread.currentThread());

        System.out.println(Thread.currentThread().getThreadGroup());

        System.out.println(Thread.currentThread().getThreadGroup().getParent());

        System.out.println(Thread.currentThread().getThreadGroup().getParent().getParent());

    }

}

运行上面代码,输出:

css 复制代码
Thread[main,5,main]

java.lang.ThreadGroup[name=main,maxpri=10]

java.lang.ThreadGroup[name=system,maxpri=10]

null

从上面代码可以看出:

  1. 主线程的线程组为main
  2. 根线程组为system

看一下ThreadGroup的源码:

java 复制代码
private ThreadGroup() {     // called from C code

        this.name = "system";

        this.maxPriority = Thread.MAX_PRIORITY;

        this.parent = null;

    }

发现ThreadGroup默认构造方法是private的,是由c调用的,创建的正是system线程组。

批量停止线程

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

示例代码:

java 复制代码
import java.util.concurrent.TimeUnit;

public class Demo4 {

    public static class R1 implements Runnable {

        @Override

        public void run() {

            Thread thread = Thread.currentThread();

            System.out.println("所属线程组:" + thread.getThreadGroup().getName() + ",线程名称:" + thread.getName());

            while (!thread.isInterrupted()) {

                ;

            }

            System.out.println("线程:" + thread.getName() + "停止了!");

        }

    }




    public static void main(String[] args) throws InterruptedException {

        ThreadGroup threadGroup1 = new ThreadGroup("thread-group-1");

        Thread t1 = new Thread(threadGroup1, new R1(), "t1");

        Thread t2 = new Thread(threadGroup1, new R1(), "t2");

        t1.start();

        t2.start();




        ThreadGroup threadGroup2 = new ThreadGroup(threadGroup1, "thread-group-2");

        Thread t3 = new Thread(threadGroup2, new R1(), "t3");

        Thread t4 = new Thread(threadGroup2, new R1(), "t4");

        t3.start();

        t4.start();

        TimeUnit.SECONDS.sleep(1);




        System.out.println("-----------threadGroup1信息-----------");

        threadGroup1.list();




        System.out.println("----------------------");

        System.out.println("停止线程组:" + threadGroup1.getName() + "中的所有子孙线程");

        threadGroup1.interrupt();

        TimeUnit.SECONDS.sleep(2);




        System.out.println("----------threadGroup1停止后,输出信息------------");

        threadGroup1.list();

    }

}

输出:

java 复制代码
所属线程组:thread-group-1,线程名称:t1

所属线程组:thread-group-1,线程名称:t2

所属线程组:thread-group-2,线程名称:t3

所属线程组:thread-group-2,线程名称:t4

-----------threadGroup1信息-----------

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]

----------------------

停止线程组:thread-group-1中的所有子孙线程

线程:t4停止了!

线程:t2停止了!

线程:t1停止了!

线程:t3停止了!

----------threadGroup1停止后,输出信息------------

java.lang.ThreadGroup[name=thread-group-1,maxpri=10]

    java.lang.ThreadGroup[name=thread-group-2,maxpri=10]

停止线程之后,通过list() 方法可以看出输出的信息中不包含已结束的线程了。

多说几句,建议大家再创建线程或者线程组的时候,给他们取一个有意义的名字,对于计算机来说,可能名字并不重要,但是在系统出问题的时候,你可能会去查看线程堆栈信息,如果你看到的都是t1、t2、t3,估计自己也比较崩溃,如果看到的是httpAccpHandler、dubboHandler类似的名字,应该会好很多。

用户线程和守护线程

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

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

下面我们来看一下守护线程的一些特性。

程序只有守护线程时,系统会自动退出

typescript 复制代码
public class Demo1 {


    public static class T1 extends Thread {

        public T1(String name) {

            super(name);

        }




        @Override

        public void run() {

            System.out.println(this.getName() + "开始执行," + (this.isDaemon() ? "我是守护线程" : "我是用户线程"));

            while (true) ;

        }

    }




    public static void main(String[] args) {

        T1 t1 = new T1("子线程1");

        t1.start();

        System.out.println("主线程结束");

    }

}

运行上面代码,结果如下:

可以看到主线程已经结束了,但是程序无法退出,原因:子线程1是用户线程,内部有个死循环,一直处于运行状态,无法结束。

再看下面的代码:

typescript 复制代码
public class Demo2 {


    public static class T1 extends Thread {

        public T1(String name) {

            super(name);

        }




        @Override

        public void run() {

            System.out.println(this.getName() + "开始执行," + (this.isDaemon() ? "我是守护线程" : "我是用户线程"));

            while (true) ;

        }

    }




    public static void main(String[] args) {

        T1 t1 = new T1("子线程1");

        t1.setDaemon(true);

        t1.start();

        System.out.println("主线程结束");

    }

}

运行结果:

程序可以正常结束了,代码中通过 t1.setDaemon(true); 将t1线程设置为守护线程,main方法所在的主线程执行完毕之后,程序就退出了。

结论:当程序中所有的用户线程执行完毕之后,不管守护线程是否结束,系统都会自动退出。

设置守护线程,需要在start()方法之前进行

java 复制代码
import java.util.concurrent.TimeUnit;


public class Demo3 {


    public static void main(String[] args) {

        Thread t1 = new Thread() {

            @Override

            public void run() {

                try {

                    TimeUnit.SECONDS.sleep(10);

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

            }

        };

        t1.start();

        t1.setDaemon(true);

    }

}

t1.setDaemon(true);是在t1的start()方法之后执行的,执行会报异常,运行结果如下:

线程daemon的默认值

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

java 复制代码
Thread parent = currentThread();

this.daemon = parent.isDaemon();

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

示例代码:

java 复制代码
public class Demo4 {

    public static class T1 extends Thread {

        public T1(String name) {

            super(name);

        }




        @Override

        public void run() {

            System.out.println(this.getName() + ".daemon:" + this.isDaemon());

        }

    }




    public static void main(String[] args) throws InterruptedException {




        System.out.println(Thread.currentThread().getName() + ".daemon:" + Thread.currentThread().isDaemon());




        T1 t1 = new T1("t1");

        t1.start();




        Thread t2 = new Thread() {

            @Override

            public void run() {

                System.out.println(this.getName() + ".daemon:" + this.isDaemon());

                T1 t3 = new T1("t3");

                t3.start();

            }

        };




        t2.setName("t2");

        t2.setDaemon(true);

        t2.start();




        TimeUnit.SECONDS.sleep(2);

    }

}

运行代码,输出:

arduino 复制代码
main.daemon:false

t1.daemon:false

t2.daemon:true

t3.daemon:true

t1是由主线程(main方法所在的线程)创建的,main线程是t1的父线程,所以t1.daemon为false,说明t1是用户线程。

t2线程调用了 setDaemon(true);将其设为守护线程,t3是由t2创建的,所以t3默认线程类型和t2一样,t2.daemon为true。

总结

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

线程安全和synchronized关键字

什么是线程安全?

当多个线程去访问同一个类(对象或方法)的时候,该类都能表现出正常的行为(与自己预想的结果一致),那我们就可以所这个类是线程安全的。

看一段代码:

java 复制代码
public class Demo1 {

    static int num = 0;




    public static void m1() {

        for (int i = 0; i < 10000; i++) {

            num++;

        }

    }




    public static class T1 extends Thread {

        @Override

        public void run() {

            Demo1.m1();

        }

    }




    public static void main(String[] args) throws InterruptedException {

        T1 t1 = new T1();

        T1 t2 = new T1();

        T1 t3 = new T1();

        t1.start();

        t2.start();

        t3.start();




        //等待3个线程结束打印num

        t1.join();

        t2.join();

        t3.join();




        System.out.println(Demo1.num);

        /**

         * 打印结果:

         * 25572

         */

    }

}

Demo1中有个静态变量num,默认值是0,m1()方法中对num++执行10000次,main方法中创建了3个线程用来调用m1()方法,然后调用3个线程的join()方法,用来等待3个线程执行完毕之后,打印num的值。我们期望的结果是30000,运行一下,但真实的结果却不是30000。上面的程序在多线程中表现出来的结果和预想的结果不一致,说明上面的程序不是线程安全的。

线程安全是并发编程中的重要关注点,应该注意到的是,造成线程安全问题的主要诱因有两点:

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

因此为了解决这个问题,我们可能需要这样一个方案,当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据 ,其他线程必须等到该线程处理完数据后再进行,这种方式有个高尚的名称叫互斥锁 ,即能达到互斥访问目的的锁,也就是说当一个共享数据被当前正在访问的线程加上互斥锁后,在同一个时刻,其他线程只能处于等待的状态,直到当前线程处理完毕释放该锁。在 Java 中,关键字 synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作)同时我们还应该注意到synchronized另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代volatile功能) ,这点确实也是很重要的。

那么我们把上面的程序做一下调整,在m1()方法上面使用关键字synchronized,如下:

java 复制代码
public static synchronized void m1() {

    for (int i = 0; i < 10000; i++) {

        num++;

    }

}

然后执行代码,输出30000,和期望结果一致。

synchronized主要有3种使用方式

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

synchronized作用于实例对象

所谓实例对象锁就是用synchronized修饰实例对象的实例方法,注意是实例方法 ,不是静态方法,如:

java 复制代码
public class Demo2 {

    int num = 0;




    public synchronized void add() {

        num++;

    }




    public static class T extends Thread {

        private Demo2 demo2;




        public T(Demo2 demo2) {

            this.demo2 = demo2;

        }




        @Override

        public void run() {

            for (int i = 0; i < 10000; i++) {

                this.demo2.add();

            }

        }

    }




    public static void main(String[] args) throws InterruptedException {

        Demo2 demo2 = new Demo2();

        T t1 = new T(demo2);

        T t2 = new T(demo2);

        t1.start();

        t2.start();




        t1.join();

        t2.join();




        System.out.println(demo2.num);

    }

}

main()方法中创建了一个对象demo2和2个线程t1、t2,t1、t2中调用demo2的add()方法10000次,add()方法中执行了num++,num++实际上是分3步,获取num,然后将num+1,然后将结果赋值给num,如果t2在t1读取num和num+1之间获取了num的值,那么t1和t2会读取到同样的值,然后执行num++,两次操作之后num是相同的值,最终和期望的结果不一致,造成了线程安全失败,因此我们对add方法加了synchronized来保证线程安全。

注意:m1()方法是实例方法,两个线程操作m1()时,需要先获取demo2的锁,没有获取到锁的,将等待,直到其他线程释放锁为止。

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

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

synchronized作用于静态方法

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

java 复制代码
public class Demo3 {

    static int num = 0;




    public static synchronized void m1() {

        for (int i = 0; i < 10000; i++) {

            num++;

        }

    }




    public static class T1 extends Thread {

        @Override

        public void run() {

            Demo3.m1();

        }

    }




    public static void main(String[] args) throws InterruptedException {

        T1 t1 = new T1();

        T1 t2 = new T1();

        T1 t3 = new T1();

        t1.start();

        t2.start();

        t3.start();




        //等待3个线程结束打印num

        t1.join();

        t2.join();

        t3.join();




        System.out.println(Demo3.num);

        /**

         * 打印结果:

         * 30000

         */

    }

}

上面代码打印30000,和期望结果一致。m1()方法是静态方法,有synchronized修饰,锁用于与Demo3.class对象,和下面的写法类似:

csharp 复制代码
public static void m1() {

    synchronized (Demo4.class) {

        for (int i = 0; i < 10000; i++) {

            num++;

        }

    }

}

synchronized同步代码块

除了使用关键字修饰实例方法和静态方法外,还可以使用同步代码块,在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了,同步代码块的使用示例如下:

java 复制代码
public class Demo5 implements Runnable {

    static Demo5 instance = new Demo5();

    static int i = 0;




    @Override

    public void run() {

        //省略其他耗时操作....

        //使用同步代码块对变量i进行同步操作,锁对象为instance

        synchronized (instance) {

            for (int j = 0; j < 10000; j++) {

                i++;

            }

        }

    }




    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(instance);

        Thread t2 = new Thread(instance);

        t1.start();

        t2.start();




        t1.join();

        t2.join();




        System.out.println(i);

    }

}

从代码看出,将synchronized作用于一个给定的实例对象instance,即当前实例对象就是锁对象,每次当线程进入synchronized包裹的代码块时就会要求当前线程持有instance实例对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待,这样也就保证了每次只有一个线程执行i++;操作。当然除了instance作为对象外,我们还可以使用this对象(代表当前实例)或者当前类的class对象作为锁,如下代码:

java 复制代码
//this,当前实例对象锁

synchronized(this){

    for(int j=0;j<1000000;j++){

        i++;

    }

}




//class对象锁

synchronized(Demo5.class){

    for(int j=0;j<1000000;j++){

        i++;

    }

}

分析代码是否互斥的方法,先找出synchronized作用的对象是谁,如果多个线程操作的方法中synchronized作用的锁对象一样,那么这些线程同时异步执行这些方法就是互斥的。如下代码:

java 复制代码
public class Demo6 {

    //作用于当前类的实例对象

    public synchronized void m1() {

    }




    //作用于当前类的实例对象

    public synchronized void m2() {

    }




    //作用于当前类的实例对象

    public void m3() {

        synchronized (this) {

        }

    }




    //作用于当前类Class对象

    public static synchronized void m4() {

    }




    //作用于当前类Class对象

    public static void m5() {

        synchronized (Demo6.class) {

        }

    }




    public static class T extends Thread{

        Demo6 demo6;




        public T(Demo6 demo6) {

            this.demo6 = demo6;

        }




        @Override

        public void run() {

            super.run();

        }

    }




    public static void main(String[] args) {

        Demo6 d1 = new Demo6();

        Thread t1 = new Thread(() -> {

            d1.m1();

        });

        t1.start();

        Thread t2 = new Thread(() -> {

            d1.m2();

        });

        t2.start();




        Thread t3 = new Thread(() -> {

            d1.m2();

        });

        t3.start();




        Demo6 d2 = new Demo6();

        Thread t4 = new Thread(() -> {

            d2.m2();

        });

        t4.start();




        Thread t5 = new Thread(() -> {

            Demo6.m4();

        });

        t5.start();




        Thread t6 = new Thread(() -> {

            Demo6.m5();

        });

        t6.start();

    }




}

分析上面代码:

  1. 线程t1、t2、t3中调用的方法都需要获取d1的锁,所以他们是互斥的
  2. t1/t2/t3这3个线程和t4不互斥,他们可以同时运行,因为前面三个线程依赖于d1的锁,t4依赖于d2的锁
  3. t5、t6都作用于当前类的Class对象锁,所以这两个线程是互斥的,和其他几个线程不互斥

线程中断的几种方式

通过一个变量控制线程中断

代码:

java 复制代码
import java.util.concurrent.TimeUnit;

public class Demo1 {




    public volatile static boolean exit = false;




    public static class T extends Thread {

        @Override

        public void run() {

            while (true) {

                //循环处理业务

                if (exit) {

                    break;

                }

            }

        }

    }




    public static void setExit() {

        exit = true;

    }




    public static void main(String[] args) throws InterruptedException {

        T t = new T();

        t.start();

        TimeUnit.SECONDS.sleep(3);

        setExit();

    }

}

代码中启动了一个线程,线程的run方法中有个死循环,内部通过exit变量的值来控制是否退出。 TimeUnit.SECONDS.sleep(3);让主线程休眠3秒,此处为什么使用TimeUnit?TimeUnit使用更方便一些,能够很清晰的控制休眠时间,底层还是转换为Thread.sleep实现的。程序有个重点:volatile关键字,exit变量必须通过这个修饰,如果把这个去掉,程序无法正常退出。volatile控制了变量在多线程中的可见性,关于volatile前面的文章中有介绍,此处就不再说了。

通过线程自带的中断标志控制

示例代码:

java 复制代码
public class Demo2 {

    public static class T extends Thread {

        @Override

        public void run() {

            while (true) {

                //循环处理业务

                if (this.isInterrupted()) {

                    break;

                }

            }

        }

    }







    public static void main(String[] args) throws InterruptedException {

        T t = new T();

        t.start();

        TimeUnit.SECONDS.sleep(3);

        t.interrupt();

    }

}

运行上面的程序,程序可以正常结束。线程内部有个中断标志,当调用线程的interrupt()实例方法之后,线程的中断标志会被置为true,可以通过线程的实例方法isInterrupted()获取线程的中断标志。

线程阻塞状态中如何中断?

示例代码:

java 复制代码
public class Demo3 {


    public static class T extends Thread {

        @Override

        public void run() {

            while (true) {

                //循环处理业务

                //下面模拟阻塞代码

                try {

                    TimeUnit.SECONDS.sleep(1000);

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

            }

        }

    }







    public static void main(String[] args) throws InterruptedException {

        T t = new T();

        t.start();

    }

}

运行上面代码,发现程序无法结束。

在此先补充几点知识:

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

那么上面代码可以调用线程的interrupt()方法来引发InterruptedException异常,来中断sleep方法导致的阻塞,调整一下代码,如下:

java 复制代码
public class Demo3 {




    public static class T extends Thread {

        @Override

        public void run() {

            while (true) {

                //循环处理业务

                //下面模拟阻塞代码

                try {

                    TimeUnit.SECONDS.sleep(1000);

                } catch (InterruptedException e) {

                    e.printStackTrace();

                    this.interrupt();

                }

                if (this.isInterrupted()) {

                    break;

                }

            }

        }

    }







    public static void main(String[] args) throws InterruptedException {

        T t = new T();

        t.start();

        TimeUnit.SECONDS.sleep(3);

        t.interrupt();

    }

}

运行结果:

java 复制代码
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.itsoku.chat05.Demo3$T.run(Demo3.java:17)

程序可以正常结束了,分析一下上面代码,注意几点:

  1. main方法中调用了t.interrupt()方法,此时线程t内部的中断标志会置为true
  2. 然后会触发run()方法内部的InterruptedException异常,所以运行结果中有异常输出,上面说了,当触发InterruptedException异常时候,线程内部的中断标志又会被清除(变为false),所以在catch中又调用了this.interrupt();一次,将中断标志置为false
  3. run()方法中通过this.isInterrupted()来获取线程的中断标志,退出循环(break)

总结

  1. 当一个线程处于被阻塞状态或者试图执行一个阻塞操作时,可以使用 Thread.interrupt()方式中断该线程,注意此时将会抛出一个InterruptedException的异常,同时中断状态将会被复位(由中断状态改为非中断状态)
  2. 内部有循环体,可以通过一个变量来作为一个信号控制线程是否中断,注意变量需要volatile修饰
  3. 文中的几种方式可以结合起来灵活使用控制线程的中断
相关推荐
customer0817 分钟前
【开源免费】基于SpringBoot+Vue.JS医院管理系统(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·开源·intellij-idea
2402_8575893627 分钟前
SpringBoot框架:作业管理技术新解
java·spring boot·后端
一只爱打拳的程序猿1 小时前
【Spring】更加简单的将对象存入Spring中并使用
java·后端·spring
假装我不帅2 小时前
asp.net framework从webform开始创建mvc项目
后端·asp.net·mvc
神仙别闹2 小时前
基于ASP.NET+SQL Server实现简单小说网站(包括PC版本和移动版本)
后端·asp.net
计算机-秋大田3 小时前
基于Spring Boot的船舶监造系统的设计与实现,LW+源码+讲解
java·论文阅读·spring boot·后端·vue
货拉拉技术3 小时前
货拉拉-实时对账系统(算盘平台)
后端
掘金酱4 小时前
✍【瓜分额外奖金】11月金石计划附加挑战赛-活动命题发布
人工智能·后端
代码之光_19804 小时前
保障性住房管理:SpringBoot技术优势分析
java·spring boot·后端