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. 文中的几种方式可以结合起来灵活使用控制线程的中断
相关推荐
㳺三才人子6 小时前
初探 Flask
后端·python·flask·html
星栈独行6 小时前
我在 Rust 全栈项目里用 JWT 做无状态认证
开发语言·后端·rust·前端框架·开源·github·web
Java爱好狂.6 小时前
Java程序员体系化学习路线(2026最新版)
java·后端·java面试·java架构师·java程序员·java八股文·java学习路线
陈随易7 小时前
Redis 8.8发布,一定要更新
前端·后端·程序员
装不满的克莱因瓶7 小时前
SpringBoot 如何将 lib 目录中jar包打包进最终的jar包里面
spring boot·后端·maven·jar·mvn
ltl8 小时前
Transformer 原论文实验结果:为什么 28.4 BLEU 足以改写路线图
后端
excel8 小时前
为什么我推荐使用 Termius:现代 SSH 工具的完整体验
前端·后端
卷毛的技术笔记9 小时前
Java后端硬核实战:用Spring AI Alibaba+Redis给LLM装上“超强记忆中枢”
java·人工智能·redis·后端·spring·ai·系统架构
IT_陈寒10 小时前
Java的Optional差点让我掉坑里,这几个坑你别踩
前端·人工智能·后端
子兮曰10 小时前
Harness 驾驭工程深度教程:从 AGENTS.md 到全链路 AI 编码基础设施
前端·后端·ai编程