并发与Java多线程(知识点大整合)

线程问题

引用

java中怎么判断一段代码时线程安全还是非线程安全_由浅入深学习Java多线程和高并发
一个ThreadLocal和面试官大战30个回合
Java并发指南1:并发基础与Java多线程.md
深入理解Java并发之synchronized实现原理
线程中断
阿里面试题,为什么wait()方法要放在同步块中?

线程

在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个jvm,每一个jvm实际上就是在操作系统中启动了一个进程。

竞态条件与临界区

参考Java并发指南1:并发基础与Java多线程.md

当多个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。
多线程同时执行下面的代码可能会出错:

public class Counter {
	protected long count = 0;
 
	public void add(long value) {
		this.count = this.count + value;
	}
}

想象下线程A和B同时执行同一个Counter对象的add()方法,我们无法知道操作系统何时会在两个线程之间切换。JVM并不是将这段代码视为单条指令来执行的,而是按照下面的顺序

从内存获取 this.count 的值放到寄存器
将寄存器中的值增加value
将寄存器中的值写回内存
观察线程A和B交错执行会发生什么
 
	this.count = 0;
   A:	读取 this.count 到一个寄存器 (0)
   B:	读取 this.count 到一个寄存器 (0)
   B: 	将寄存器的值加2
   B:	回写寄存器值(2)到内存. this.count 现在等于 2
   A:	将寄存器的值加3

由于两个线程是交叉执行的,两个线程从内存中读出的初始值都是0。然后各自加了2和3,并分别写回内存。最终的值可能并不是期望的5,而是最后写回内存的那个线程的值,上面例子中最后写回内存可能是线程A,也可能是线程B
所以这就是要保证线程安全的一个很重要的一部分:要保证代码原子性,即要保证当前代码执行时,不会有对象再使用改代码。为了解决这个并发问题,锁就出现了。可以使用synchronized关键字, synchronized能够实现原子性(同步)。但锁住的区域也仅仅是当前实例。下面会详细介绍

同步 异步

同步异步 , 举个例子来说,一家餐厅吧来了5个客人,同步的意思就是说,来第一个点菜,点了个鱼,好,让 厨师去捉鱼杀鱼,过了半小时鱼好了给第一位客人,开始下位一位客人,就这样一个一个来,按顺序来
相同, 异步呢,异步的意思就是来第一位客人,点什么,点鱼,给它一个牌子,让他去一边等吧,下一位客人接着点菜,点完接着点让厨师做去吧,哪个的菜先好就先端出来,

同步的优点是:同步是按照顺序一个一个来,不会乱掉,更不会出现上面代码没有执行完就执行下面的代码, 缺点:是解析的速度没有异步的快;

异步的优点是:异步是接取一个任务,直接给后台,在接下一个任务,一直一直这样,谁的先读取完先执行谁的, 缺点:没有顺序 ,谁先读取完先执行谁的

总结:所以要保证代码执行的原子性,就必须一个一个来,也就是需要将临界区进行同步操作。使用synchronized(同步)完成

线程的两种创建方式

  • 实现Runnable接口
//创建线程的两种方法
public class RunnableStyle implements Runnable{    
	public static void main(String[] args) {        
		Thread thread = new Thread(new RunnableStyle());        
		thread.start();    
		}    
	@Override    
	public void run() {       
		System.out.println("用Runnable方法实现线程");    
	}
}
  • 继承Thread
public class ThreadStyle extends Thread{    
@Override    
	public void run() {        
		System.out.println("用Thread类实现线程");    
	}    
	public static void main(String[] args) {       
		 new ThreadStyle().start();    
		 }
}

线程的状态和优先级

线程优先级1 到 10 ,其中 1 是最低优先级,10 是最高优先级。
状态

new(新建)
runnnable(可运行)
blocked(阻塞)
waiting(等待)
time waiting (定时等待)
terminated(终止)

在这里插入图片描述

Thread重要方法

wait()和notify()

参考链接使用wait/notify/notifyAll实现线程间通信的几点重要说明

为什么wait()方法不是使线程等待,而是控制对象锁的呢

注意在进行wait()和notify()方法时,都需要将其放入同一个同步对象锁中。例如下面的static AtomicInteger count = new AtomicInteger(0);对象。下面的代码都对该对象进行加锁。为什么wait()方法不是使线程等待,而是控制对象锁的呢。首先synchronized获取对象锁锁住的谁什么?当然是对象,而不是下面的代码,但是你只有获取目标对象的锁时才能执行代码?为什么不直接使用synchronized锁住代码片段呢?

思考:锁住代码片段是为了防止不同线程同时改变共享变量,会导致数据不一致,所以synchronized方法锁住代码,就是为了保证代码的原子性。但不同线程之间并不是只在使用同一个代码片段才会爆发线程安全,例如A方法实现count++,B方法实现count–。当count为0时,B实例的线程等待,等待A实例线程的唤醒。这就涉及到了不同线程之间的通信,对同一个数据进行的不同操作,那么我们当然是要锁住对象,只有获取对象锁时才能执行对对象修改的操作就可以了。

其实对象锁就是一种线程之间的通信工具,只有获取对象锁时才能执行对对象的操作,所以当然要把对对象的操作。对对象锁的加锁和解锁对应使用它的线程的等待和唤醒。wait不直接不直接作用于线程就是为了让对象锁起到一个在线程之间进行交流的目的。

需要注意:

  1. 如果将synchronized的锁改为XXX.class,再运行程序,会报IllegalMonitorStateException。原因是wait方法需要精确到哪个对象,而使用方法锁精确的对象即为拿到锁的对象,类锁无法锁定对象
  2. Integer型变量在执行赋值语句的时候,其实是创建了一个新的对象。因为基本数据类型的包装类确实是不可变的,重新赋值之后就新创建了一个对象,在代码中这几个类都被定义为Final。简单的说, 在改变值前后,count不是同一个对象了,所以两个方法同步失败。相同的悲剧还有可能出现在Boolean或者String类型的时候。
    一个解决方案是采用java.util.concurrent.atomic中对应的类型,比如这里就应该是AtomicInteger 。采用AtomicInteger 类型,可以保证对它的修改不会产生新的对象。

import java.util.concurrent.atomic.AtomicInteger;

public class TimedWaitingAndBlocked {
        static AtomicInteger count = new AtomicInteger(0);

        public static void main(String[] args) throws Exception {
            sub ss=new sub();
            add aa=new add();
            Thread thread1 = new Thread(aa, "t1线程");
            Thread thread2 = new Thread(ss, "t2线程");
            thread1.start();
            thread2.start();
//        Thread.sleep(1000);
            System.out.println(thread1.getState());
            System.out.println(thread2.getState());


        }


        static class sub implements Runnable {
            @Override
            public void run() {
                while(true) {
                synchronized (count) {
                        System.out.println(Thread.currentThread().getName());
                        while (count.intValue() <= 0) {
                            try {
                                System.out.println("等待加法…………………………");
                                Thread.sleep(1000);
                                count.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }

                        }
                    count.set(count.intValue() - 1);
                    System.out.println(count.intValue());
                    }

                }


            }


        }

       static class add implements Runnable {

            public void run() {
                while (true) {
                    System.out.println(Thread.currentThread().getName());
                    synchronized (count) {
                        try {
                            count.set(count.intValue() + 1);
                            System.out.println(count.intValue());
                                Thread.sleep(1000);
                                System.out.println("启动减法…………………………");
                                count.notify();

                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }



Join()方法

join()方法是Thread类中的一个方法,该方法的定义是等待该线程终止。其实就是join()方法将挂起调用线程的执行,直到被调用的对象完成它的执行

参考链接常见多线程面试题之Thread的join()方法

join方法用线程对象调用,如果在一个线程A中调用另一个线程B的join方法,线程A将会等待线程B执行完毕后再执行。
yield可以直接用Thread类调用,yield让出CPU执行权给同等级的线程,如果没有相同级别的线程在等待CPU的执行权,则该线程继续执行。

废弃thread.stop()

看这里Thread.stop()为何废弃

     * @deprecated This method is inherently unsafe.  Stopping a thread with
     *       Thread.stop causes it to unlock all of the monitors that it
     *       has locked (as a natural consequence of the unchecked
     *       <code>ThreadDeath</code> exception propagating up the stack).  If
     *       any of the objects previously protected by these monitors were in
     *       an inconsistent state, the damaged objects become visible to
     *       other threads, potentially resulting in arbitrary behavior.  Many
     *       uses of <code>stop</code> should be replaced by code that simply
     *       modifies some variable to indicate that the target thread should
     *       stop running.  The target thread should check this variable
     *       regularly, and return from its run method in an orderly fashion
     *       if the variable indicates that it is to stop running.  If the
     *       target thread waits for long periods (on a condition variable,
     *       for example), the <code>interrupt</code> method should be used to
     *       interrupt the wait.

Thread.stop()停掉一个线程将会导致所有已锁定的监听器被解锁(解锁的原因是当threaddeath异常在堆栈中传播时,监视器被解锁),这个之前被监听器锁定的对象被解锁,其他线程就能随意操作这个对象,将导致任何可能的结果。

   * An application should not normally try to catch
     * <code>ThreadDeath</code> unless it must do some extraordinary
     * cleanup operation (note that the throwing of
     * <code>ThreadDeath</code> causes <code>finally</code> clauses of
     * <code>try</code> statements to be executed before the thread
     * officially dies).  If a <code>catch</code> clause catches a
     * <code>ThreadDeath</code> object, it is important to rethrow the
     * object so that the thread actually dies.

Thread.stop()会抛出ThreadDeath异常。但这个异常是继承自Error类,而不是Exception。尽管他是一个“正常事件”,但它正常情况下不应该使用try,catch。因为处理这个异常会导致try()里的finally()在线程真正死亡之前执行。所以如果catch了一个ThreadDeath异常,需要将它重新扔出以确定它真正死亡。

官方给出的网页说明了不能捕获ThreadDeath异常并修复对象的原因:
一个线程几乎可以在任何地方抛出一个ThreadDeath异常。考虑到这一点,所有同步的方法和块都必须详细研究。
一个线程可以抛出第二个ThreadDeath异常,同时从第一个线程清除(在catch或finally子句中)。清理将不得不重复,直到它成功。确保这一点的代码将非常复杂。
————————————————
版权声明:本文为CSDN博主「ming-world」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Zheng_junming/article/details/80064252

wait()、sleep()、join()、与interruptedException异常

参考wait()、sleep()、join()、与interruptedException异常

InterruptedException 线程堵塞异常

主要的抛出该异常的方法有

1、java.lang.object的wait():会进入等待区等待
2、java.lang.thread的sleep():会睡眠执行参数内所设置的时间。
3、java.lang.join():会等待到指定的线程结束为止。

因为当这些方法执行结束后,该线程会重新启动,就的可能会出现线程堵塞。 所有这些方法需要处理interrptdeException异常
————————————————
版权声明:本文为CSDN博主「尘锿」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_36776347/article/details/78886957

wait()、sleep()

sleep()是使线程暂停执行一段时间的方法。wait()也是一种使线程暂停执行的方法。例如,当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。并且可以调用notify()方法或者notifyAll()方法通知正在等待的其他线程。notify()方法仅唤醒一个线程(等待队列中的第一个线程)并允许他去获得锁。notifyAll()方法唤醒所有等待这个对象的线程并允许他们去竞争获得锁。具体区别如下:

  1. 原理不同sleep()方法是Thread类的静态方法,是线程用来控制自身流程的,他会使此线程暂停执行一段时间,而把执行机会让给其他线程,等到计时时间一到,此线程会自动苏醒。例如,当线程执行报时功能时,每一秒钟打印出一个时间,那么此时就需要在打印方法前面加一个sleep()方法,以便让自己每隔一秒执行一次,该过程如同闹钟一样。而wait()方法是object类的方法,用于线程间通信,这个方法会使当前拥有该对象锁的进程等待,直到其他线程调用notify()方法或者notifyAll()时才醒来,不过开发人员也可以给他指定一个时间,自动醒来。
  1. 对锁的处理机制不同
    由于sleep()方法的主要作用是让线程暂停执行一段时间,时间一到则自动恢复不涉及线程间的通信因此,调用sleep()方法并不会释放锁而wait()方法则不同,当调用wait()方法后,线程会释放掉他所占用的锁,从而使线程所在对象中的其他synchronized数据可以被其他线程使用

  2. 使用区域不同。
    wait()方法必须放在同步控制方法和同步代码块中使用
    ,原因如下。sleep()方法则可以放在任何地方使用。wait()、sleep()方法必须捕获异常,而notify()、notifyAll()不需要捕获异常。由于sleep不会释放锁标志,容易导致死锁问题的发生,因此一般情况下,推荐使用wait()方法。
    ———————————————— 版权声明:本文为CSDN博主「种向日葵的小仙女」的原创文章,遵循CC 4.0
    BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/qiuchaoxi/article/details/79837568

需要特别理解的一点是,与sleep方法不同的是wait方法调用完成后,线程将被暂停,但wait方法将会释放当前持有的监视器锁(monitor),直到有线程调用notify/notifyAll方法后方能继续执行,而sleep方法只让线程休眠并不释放锁。同时notify/notifyAll方法调用后,并不会马上释放监视器锁,而是在相应的synchronized(){}/synchronized方法执行结束后才自动释放锁
————————————————
版权声明:本文为CSDN博主「zejian_」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/javazejian/article/details/72828483

阿里面试题,为什么wait()方法要放在同步块中?

引用链接阿里面试题,为什么wait()方法要放在同步块中?

经过一番谷歌之后,找到了答案。
Lost Wake-Up Problem
事情得从一个多线程编程里面臭名昭著的问题"Lost wake-up problem"说起。
这个问题并不是说只在Java语言中会出现,而是会在所有的多线程环境下出现。
假如有两个线程,一个消费者线程,一个生产者线程。生产者线程的任务可以简化成将count加一,而后唤醒消费者;消费者则是将count减一,而后在减到0的时候陷入睡眠:

生产者伪代码:

count+1;
notify();

消费者伪代码:

while(count<=0)
 	wait()
count--

熟悉多线程的朋友一眼就能够看出来,这里面有问题。什么问题呢?

生产者是两个步骤:

count+1;
notify();

消费者也是两个步骤:

检查count值;
睡眠或者减一;

万一这些步骤混杂在一起呢?比如说,初始的时候count等于0,这个时候消费者检查count的值,发现count小于等于0的条件成立;就在这个时候,发生了上下文切换,生产者进来了,噼噼啪啪一顿操作,把两个步骤都执行完了,也就是发出了通知,准备唤醒一个线程。这个时候消费者刚决定睡觉,还没睡呢,所以这个通知就会被丢掉。紧接着,消费者就睡过去了……
在这里插入图片描述

现在我们应该就能够看到,问题的根源在于,消费者在检查count到调用wait()之间,count就可能被改掉了。这就是一种很常见的竞态条件。
很自然的想法是,让消费者和生产者竞争一把锁,竞争到了的,才能够修改count的值。

所以,我们可以总结到,为了避免出现这种lost wake up问题,在这种模型之下,总应该将我们的代码放进去的同步块中。

Java强制我们的wait()/notify()调用必须要在一个同步块中,就是不想让我们在不经意间出现这种lost wake up问题。

interrupt()中断线程

参考链接
使用interrupt()中断线程
线程中断与synchronized

//中断线程(实例方法)
public void Thread.interrupt();

//判断线程是否被中断(实例方法)
public boolean Thread.isInterrupted();

//判断是否被中断并清除当前中断状态(静态方法)
public static boolean Thread.interrupted();

当一个线程运行时,另一个线程可以调用对应的Thread对象的interrupt()方法来中断它,该方法只是在目标线程中设置一个标志,表示它已经被中断,并立即返回。这里需要注意的是,如果只是单纯的调用interrupt()方法,线程并没有实际被中断,会继续往下执行。

第一种:当前线程处于阻塞的状态下执行阻断

public class SleepInterrupt extends Object implements Runnable{  
     public void run(){  
         try{  
             System.out.println("in run() - about to sleep for 20 seconds");  
             Thread.sleep(20000);  
             System.out.println("in run() - woke up");  
         }catch(InterruptedException e){  
             System.out.println("in run() - interrupted while sleeping");  
             //处理完中断异常后,返回到run()方法人口,  
             //如果没有return,线程不会实际被中断,它会继续打印下面的信息  
             return;    
         }  
         System.out.println("in run() - leaving normally");  
     }  
   
   
     public static void main(String[] args) {  
         SleepInterrupt si = new SleepInterrupt();  
         Thread t = new Thread(si);  
         t.start();  
         //主线程休眠2秒,从而确保刚才启动的线程有机会执行一段时间  
         try {  
             Thread.sleep(2000);   
         }catch(InterruptedException e){  
             e.printStackTrace();  
         }  
         System.out.println("in main() - interrupting other thread");  
         //中断线程t  
         t.interrupt();  
         System.out.println("in main() - leaving");  
     }  
 }  

主线程启动新线程后,自身休眠2秒钟,允许新线程获得运行时间。新线程打印信息“about to sleep for 20 seconds”后,继而休眠20秒钟,大约2秒钟后,main线程通知新线程中断,那么新线程的20秒的休眠将被打断,从而抛出InterruptException异常,执行跳转到catch块,打印出“interrupted while sleeping”信息,并立即从run()方法返回,然后消亡,而不会打印出catch块后面的“leaving normally”信息。
请注意:由于不确定的线程规划,上图运行结果的后两行可能顺序相反,这取决于主线程和新线程哪个先消亡。但前两行信息的顺序必定如上图所示。
另外,如果将catch块中的return语句注释掉,则线程在抛出异常后,会继续往下执行,而不会被中断,从而会打印出”leaving normally“信息。

第二种:线程试图执行一个阻塞操作时进行阻断

public class PendingInterrupt extends Object {  
 public static void main(String[] args){  
     //如果输入了参数,则在mian线程中中断当前线程(亦即main线程)  
     if( args.length > 0 ){  
         Thread.currentThread().interrupt();  
     }   
     //获取当前时间  
     long startTime = System.currentTimeMillis();  
     try{  
         Thread.sleep(2000);  
         System.out.println("was NOT interrupted");  
     }catch(InterruptedException x){  
         System.out.println("was interrupted");  
     }  
     //计算中间代码执行的时间  
     System.out.println("elapsedTime=" + ( System.currentTimeMillis() - startTime));  
 }  

如果PendingInterrupt不带任何命令行参数,那么线程不会被中断,最终输出的时间差距应该在2000附近(具体时间由系统决定,不精确),如果PendingInterrupt带有命令行参数,则调用中断当前线程的代码,但main线程仍然运行,最终输出的时间差距应该远小于2000,因为线程尚未休眠,便被中断,因此,一旦调用sleep()方法,会立即打印出catch块中的信息。执行结果如下:

第三种:当线程处于运行状态时进行阻断

public class InterruputThread {
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(){
            @Override
            public void run(){
                while(true){
                    System.out.println("未被中断");
                }
            }
        };
        t1.start();
        TimeUnit.SECONDS.sleep(2);
        t1.interrupt();

        /**
         * 输出结果(无限执行):
             未被中断
             未被中断
             未被中断
             ......
         */
    }
}

虽然我们调用了interrupt方法,但线程t1并未被中断,因为处于非阻塞状态的线程需要我们手动进行中断检测并结束程序,改进后代码如下:

public class InterruputThread {
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(){
            @Override
            public void run(){
                while(true){
                    //判断当前线程是否被中断
                    if (this.isInterrupted()){
                        System.out.println("线程中断");
                        break;
                    }
                }

                System.out.println("已跳出循环,线程中断!");
            }
        };
        t1.start();
        TimeUnit.SECONDS.sleep(2);
        t1.interrupt();

        /**
         * 输出结果:
            线程中断
            已跳出循环,线程中断!
         */
    }
}
总结一下中断两种情况

注意非阻塞状态调用interrupt()并不会导致中断状态重置。
一种是当线程处于阻塞状态或者试图执行一个阻塞操作时,我们可以使用实例方法interrupt()进行线程中断,执行中断操作后将会抛出interruptException异常(该异常必须捕捉无法向外抛出)并将中断状态复位,另外一种是当线程处于运行状态时,我们也可调用实例方法interrupt()进行线程中断,但同时必须手动判断中断状态,并编写中断线程的代码(其实就是结束run方法体的代码)。

第四种:线程等待执行时与阻断

事实上线程的中断操作对于正在等待获取的锁对象的synchronized方法或者代码块并不起作用,也就是对于synchronized来说,如果一个线程在等待锁,那么结果只有两种,要么它获得这把锁继续执行,要么它就保存等待,即使调用中断线程的方法,也不会生效

线程池

参考Java 多线程:彻底搞懂线程池

线程池的思想是在进程开始时创建一定数量的线程,并放入到池中等待工作。当服务器收到请求时,它会唤醒池中的一个线程(如果有线程),并将处理的请求传递给它。一旦线程完成任务。它就会回到池中继续等待工作。如果线程池中没有空闲线程,就一直等到有空闲线程为止。

优势
(1)降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
(2)提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
(3)提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

参数

建议直接看源码

  • corePoolSize(必需):核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout设置为 true 时,核心线程也会超时回收。
  • maximumPoolSize(必需):线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。
  • keepAliveTime(必需):线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将
  • allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
  • unit(必需):指定keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
  • workQueue(必需):任务队列。通过线程池的 execute() 方法提交的 Runnable
    对象将存储在该参数中。其采用阻塞队列实现。
  • threadFactory(可选):线程工厂。用于指定为线程池创建新线程的方式。
  • handler(可选):拒绝策略。当达到最大线程数时需要执行的饱和策略。
    ————————————————
    版权声明:本文为CSDN博主「孙强 Jimmy」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/u013541140/article/details/95225769
    看这个超棒的流程图!!!!
    在这里插入图片描述

线程池的使用方法

// 创建线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(CORE_POOL_SIZE,
                                             MAXIMUM_POOL_SIZE,
                                             KEEP_ALIVE,
                                             TimeUnit.SECONDS,
                                             sPoolWorkQueue,
                                             sThreadFactory);
// 向线程池提交任务
threadPool.execute(new Runnable() {
    @Override
    public void run() {
        ... // 线程执行的任务
    }
});
// 关闭线程池
threadPool.shutdown(); // 设置线程池的状态为SHUTDOWN,然后中断所有没有正在执行任务的线程
threadPool.shutdownNow(); // 设置线程池的状态为 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表

下面的大部分内容是直接从源码获得,建议不懂部分直接看源码

线程池中 corePoolSize, maximumPoolSize, poolSize理解

corePoolSize 线程池基本大小
maximumPoolSize 线程池最大数
poolSize: 当前线程数
任何{@link BlockingQueue}都可以用来传输和保存提交的任务。此队列的使用与池大小交互:

  1. 当poolSize < corePoolSize, 无论是否其中有空闲的线程,都会给新的任务产生新的线程
  2. 当corePoolSize<poolSize<maximumPoolSize, 而又有空闲线程,就给新任务使用空闲线程,如没有空闲线程,则放入workQueue中,当workQueue已满,则创建新的线程
  3. 当poolSize>maximumPoolSize,那么后面来的任务默认都会被拒绝–通常都会报异常。

Keep-alive times

如果池当前有多个corePoolSize线程,如果多余的线程空闲时间超过keepAliveTime,则将终止这些线程。这提供了一种在池未被积极使用时减少资源消耗的方法。使用Long.MAX值可以有效地禁止空闲线程在关闭之前终止。默认情况下,“Keep-alive”策略仅在有多个corePoolSize线程时应用。

排队的策略

队列的一般策略有三种:

  1. 直接切换
    工作队列的一个很好的默认选择是{@link SynchronousQueue},它将任务交给线程而不以其他方式持有它们。在这里,如果没有线程可以立即运行任务,那么将任务排队的尝试将失败因此将构造一个新线程。此策略可避免在处理可能具有内部依赖关系的请求集时发生锁定。直接切换通常需要无限的MaximumPoolSize以避免拒绝新提交的任务。这反过来又允许了当命令继续以平均快于其处理速度的速度到达时,线程无限增长的可能性。
 * <li> <em> Direct handoffs.</em> A good default choice for a work
 * queue is a {@link SynchronousQueue} that hands off tasks to threads
 * without otherwise holding them. Here, an attempt to queue a task
 * will fail if no threads are immediately available to run it, so a
 * new thread will be constructed. This policy avoids lockups when
 * handling sets of requests that might have internal dependencies.
 * Direct handoffs generally require unbounded maximumPoolSizes to
 * avoid rejection of new submitted tasks. This in turn admits the
 * possibility of unbounded thread growth when commands continue to
 * arrive on average faster than they can be processed.  </li>
  1. 无界队列。
    使用无界队列(例如{@link LinkedBlockingQueue}没有预定义的容量)将导致在所有corePoolSize线程都忙时,新任务在队列中等待。因此,创建的线程不会超过corePoolSize(因此,maximumPoolSize的值没有任何影响。)当每个任务完全独立于其他任务时,这可能是合适的,因此任务不能影响其他任务的执行;例如,在网页服务器中。虽然这种排队方式有助于消除瞬时的请求突发,但当命令继续以平均快于其处理速度的速度到达时,它承认了无限工作队列增长的可能性
<li><em> Unbounded queues.</em> Using an unbounded queue (for
 * example a {@link LinkedBlockingQueue} without a predefined
 * capacity) will cause new tasks to wait in the queue when all
 * corePoolSize threads are busy. Thus, no more than corePoolSize
 * threads will ever be created. (And the value of the maximumPoolSize
 * therefore doesn't have any effect.)  This may be appropriate when
 * each task is completely independent of others, so tasks cannot
 * affect each others execution; for example, in a web page server.
 * While this style of queuing can be useful in smoothing out
 * transient bursts of requests, it admits the possibility of
 * unbounded work queue growth when commands continue to arrive on
 * average faster than they can be processed.  </li>
  1. 有界队列。
    有界队列(例如,{@link ArrayBlockingQueue})在与有限的maximumPoolSizes一起使用时有助于防止资源耗尽,但可能更难调整和控制。队列大小和最大池大小可以相互交换使用大队列和小池可以最大限度地减少CPU使用、操作系统资源和上下文切换开销,但也可能导致人为的低吞吐量如果任务经常阻塞(例如,如果它们是I/O绑定的),系统可能能够为更多的线程安排时间,而不是您允许的时间使用小队列通常需要更大的池大小,这使得cpu更繁忙但可能会遇到不可接受的调度开销,这也会降低吞吐量。
 * <li><em>Bounded queues.</em> A bounded queue (for example, an
 * {@link ArrayBlockingQueue}) helps prevent resource exhaustion when
 * used with finite maximumPoolSizes, but can be more difficult to
 * tune and control.  Queue sizes and maximum pool sizes may be traded
 * off for each other: Using large queues and small pools minimizes
 * CPU usage, OS resources, and context-switching overhead, but can
 * lead to artificially low throughput.  If tasks frequently block (for
 * example if they are I/O bound), a system may be able to schedule
 * time for more threads than you otherwise allow. Use of small queues
 * generally requires larger pool sizes, which keeps CPUs busier but
 * may encounter unacceptable scheduling overhead, which also
 * decreases throughput.  </li>

拒绝

在方法{@link#execute(Runnable)}中提交的新任务将被拒绝。

  1. 当Executor关闭时
  2. 当Executor对最大线程和工作队列容量使用有限边界时,并且是饱和的。

通常有四种解决策略

  1. 直接抛出RejectedExecutionException异常
  2. 在{@link ThreadPoolExecutor.callerrunpolicy}中,调用{@code execute}本身的线程运行任务。即在调用者的线程中执行任务,除非执行器已经关闭,如果关闭,任务被丢弃。这提供了一个简单的反馈控制机制,可以降低提交新任务的速度
  3. 直接丢弃
  4. 在{@link ThreadPoolExecutor.DiscardOldestPolicy}中,如果未关闭执行器,则会丢弃工作队列头部的任务,然后重试执行(可能再次失败,导致重复执行)

结束

程序中不再引用且没有剩余线程的池将自动{@code shutdown}。如果您想确保即使用户忘记调用{@link#shutdown},也会回收未引用的池,那么您必须通过设置适当的保持活动时间、使用零核心线程的下限和/或设置{@link#allowCoreThreadTimeOut(布尔值)}来安排未使用的线程最终消亡

shutdown():

启动有序关闭,其中执行以前提交的任务,但不接受新任务。如果调用已经关闭,则调用没有其他效果。此方法不会等待以前提交的任务完成执行

shutdownNow():
尝试停止所有正在执行的任务,停止正在等待的任务的处理,并返回正在等待执行的任务的列表从该方法返回时,这些任务将从任务队列中排出(删除)。此方法不会等待活跃地执行的任务终止。使用{@link#awaitTermination}来完成。除了尽最大努力停止处理正在执行的任务之外没有其他保证。此实现通过{@link Thread#interrupt}取消任务,因此任何未能响应中断的任务都可能永远不会终止

四种线程池

在这里插入图片描述
其实 Executors 的 4 个功能线程有如下弊端:
FixedThreadPool 和 SingleThreadExecutor:主要问题是堆积的请求处理队列均采用 LinkedBlockingQueue,可能会耗费非常大的内存,甚至 OOM。
CachedThreadPool 和 ScheduledThreadPool:主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。
————————————————
版权声明:本文为CSDN博主「孙强 Jimmy」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u013541140/article/details/95225769

总结

  1. 了解队列策略、核心线程数(corePoolSize)和最大线程数(maximumPoolSize)之间的关系,用于调整任务和线程的关系。
  2. 了解线程的回收机制
  3. 了解任务执行被拒绝的解决策略
  4. 了解线程池的结束策略

ThreadLocal

引用链接一个ThreadLocal和面试官大战30个回合

ThreadLocal是用于存储线程在任务执行过程便于获取参数和设置参数的线程变量,它是跟线程紧密相关的;它的出现是为了解决多线程环境下的变量访问并发问题;所以单线程的任务或进程根本用不着它,直接用全局实例变量或类变量即可。

Threadlocal 主要用来做线程变量的隔离,这么说可能不是很直观。
还是说前面提到的例子,我们程序在处理用户请求的时候,通常后端服务器是有一个线程池,来一个请求就交给一个线程来处理,那为了防止多线程并发处理请求的时候发生串数据,比如AB线程分别处理安琪拉和妲己的请求,A线程本来处理安琪拉的请求,结果访问到妲己的数据上了,把妲己支付宝的钱转走了。
所以就可以把安琪拉的数据跟A线程绑定,线程处理完之后解除绑定。
在这里插入图片描述

强弱引用

  • 强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它,当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
  • 如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。
  • 弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描内存区域时,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
  • 虚引用顾名思义,就是形同虚设。与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

synchronized

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

public class day16 extends Thread{
    static int ss=1;
    public  synchronized void increase(){
        System.out.println("start");
        ss=ss+1;
        System.out.println(ss);
        System.out.println("over");
    }
    @Override
    public void run() {
        increase();
    }
    public static void main(String[] args) {
            new day16().start();
            new day16().start();
            new day16().start();
    }
}

结果:

start
start
3
over
start
4
over
2
over

因为new day16()每次创建的都是不同对象,不同对象取得了各自的锁,所以可以在第一个对象执行increase方法时,第二个对象也进入increase方法。因为他们的锁是各自的。所以如果想要在第一个对象执行该方法时,不能再有对象进入该方法,可以将increase设置为静态方法,那么每个对象都共用一个increase方法,也就公用一把锁,保证了代码执行的原子性。

    public static synchronized void increase(){
        System.out.println("start");
        ss=ss+1;
        System.out.println(ss);
        System.out.println("over");
    }

结果:

start
2
over
start
3
over
start
4
over

注意: 这里我们还需要意识到,当一个线程正在访问一个对象的 synchronized 实例方法,那么其他线程不能访问该对象的其他 synchronized 方法,毕竟一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized实例方法,但是其他线程还是可以访问该实例对象的其他非synchronized方法
———————————————— 版权声明:本文为CSDN博主「zejian_」的原创文章,遵循CC 4.0
BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/javazejian/article/details/72828483

synchronized的四种用法

【并发编程】java并发编程之synchronized

1.锁定普通的方法
2.锁定静态方法
3.锁定当前类
4.锁定代码块

详细介绍:

  1. 锁定普通的方法
    就是在add()前面加上synchronized关键字,锁住的区域也仅仅是当前实例。例如上面的第一个代码。需要注意的一点是这样加锁是不能保证线程安全的,因为普通方法加锁只能锁住当前实例,而我在main方法中实例化了2个MySynchronized对象,所以这里有两把锁,分别锁着两个不同的实例,这两个线程之间还是存在问题。
    ———————————————— 版权声明:本文为CSDN博主「流星007」的原创文章,遵循CC 4.0
    BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/qq_33220089/article/details/102934258

  2. 锁定静态方法
    其实就是在方法前加一个static关键字,这种方式锁定的是当前类,不管你创建多少实例,它都能保证前程的互斥性。例如上面的第2个代码。由于这种加锁方式是锁定当前类,虽然保证了所有线程的互斥性,但是性能极低,使用的还需慎重。

  3. 代码块锁定(锁定当前类)
    刚刚介绍加锁带static关键字的方法就是锁定当前类,为什么这里还要介绍一下锁定当前类呢?那是因为通过锁定static方法只是其中的一种方式,还可以通过代码块来锁定当前类,请看代码:

package com.ymy.utils;
 
/**
 * synchronized锁
 */
public class MySynchronized {
    public static long sum = 0;
 
    public void add() {
        synchronized (MySynchronized.class) {//利用反射机制,直接用类名.class 来获取 Class对象:表示所订的是MySynchronized类,而不针对特定对象。
            for (long i = 0; i < 10000; i++) {
                sum += 1;
            }
        }
 
    }
 
    public static void main(String[] args) throws InterruptedException {
 
        Thread t1 = new Thread(() -> {//lamba表达式,将函数作为参数进行传递
            new MySynchronized().add();
        });
        t1.start();
        Thread t2 = new Thread(() -> {
            new MySynchronized().add();
        });
        t2.start();
        t1.join();
        t2.join();
        System.out.println("累加总和:" + sum);
 
    }
}

这种方式就是通过代码块锁定当前类,效果和锁定static方法是一样的,这样做的效果会比第一种好一点,为什么这么说呢?因为static锁定的是整个方法,而代码块锁定的是你需要锁定的某一行或者某几行代码,效果比较好。
不同实现方式:

public class AccountingSync implements Runnable{
    static AccountingSync instance=new AccountingSync();
    static int i=0;
    @Override
    public void run() {
        //省略其他耗时操作....
        //使用同步代码块对变量i进行同步操作,锁对象为instance
        synchronized(instance){
            for(int j=0;j<1000000;j++){
                    i++;
              }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(instance);//传入Runnable对象,容易个实例
        Thread t2=new Thread(instance);
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }
}

当然除了instance作为对象外,我们还可以使用this对象(代表当前实例)或者当前类的class对象作为锁,如下代码:

//this,当前实例对象锁
synchronized(this){
    for(int j=0;j<1000000;j++){
        i++;
    }
}
//class对象锁
synchronized(AccountingSync.class){
    for(int j=0;j<1000000;j++){
        i++;
    }
}

代码块锁定(对象): 锁定一个你指定的对象,所有引用此对象的线程保持同步(互斥)不同对象互不影响。

package com.ymy.utils;
 
/**
 * synchronized锁
 */
public class MySynchronized {
    public static long sum = 0;

    private static  String lock1 = "lock1";
 
    private static  String lock2 = "lock2";
 
    public  void add() throws InterruptedException {
        synchronized (lock1) {
            System.out.println("这里锁住的对象是:lock1,线程名:"+Thread.currentThread().getName());
            Thread.sleep(5000);
            System.out.println("lock1休眠结束");
        }
 
        synchronized (lock2) {
            System.out.println("这里锁住的对象是:lock2,线程名:"+Thread.currentThread().getName());
            Thread.sleep(5000);
            System.out.println("lock2休眠结束");
        }
 
    }
 
    public static void main(String[] args) throws InterruptedException {
 
        Thread t1 = new Thread(() -> {
            try {
                new MySynchronized().add();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t1.start();
        Thread t2 = new Thread(() -> {
            try {
                new MySynchronized().add();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t2.start();
 
    }
}

运行结果:

这里锁住的对象是:lock1,线程名:Thread-0
lock1休眠结束
这里锁住的对象是:lock2,线程名:Thread-0
这里锁住的对象是:lock1,线程名:Thread-1
lock2休眠结束
lock1休眠结束
这里锁住的对象是:lock2,线程名:Thread-1
lock2休眠结束

如果lock1不加锁

public class MySynchronized {
    public static long sum = 0;


    private static  String lock1 = "lock1";

    private static  String lock2 = "lock2";

    public  void add() throws InterruptedException {
//        synchronized (lock1) {
            System.out.println("这里锁住的对象是:lock1,线程名:"+Thread.currentThread().getName());
            Thread.sleep(5000);
            System.out.println("lock1休眠结束"+Thread.currentThread().getName());
//        }

        synchronized (lock2) {
            System.out.println("这里锁住的对象是:lock2,线程名:"+Thread.currentThread().getName());
            Thread.sleep(5000);
            System.out.println("lock2休眠结束");
        }

    }

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

        Thread t1 = new Thread(() -> {
            try {
                new MySynchronized().add();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t1.start();
        Thread t2 = new Thread(() -> {
            try {
                new MySynchronized().add();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t2.start();

    }
}

执行结果:

这里锁住的对象是:lock1,线程名:Thread-0
这里锁住的对象是:lock1,线程名:Thread-1
lock1休眠结束Thread-1
lock1休眠结束Thread-0
这里锁住的对象是:lock2,线程名:Thread-1
lock2休眠结束
这里锁住的对象是:lock2,线程名:Thread-0
lock2休眠结束

Process finished with exit code 0

Java虚拟机对synchronized的优化

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

锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级,关于重量级锁,前面我们已详细分析过,下面我们将介绍偏向锁和轻量级锁以及JVM的其他优化手段,这里并不打算深入到每个锁的实现和转换过程更多地是阐述Java虚拟机所提供的每个锁的核心优化思想,毕竟涉及到具体过程比较繁琐,如需了解详细过程可以查阅《深入理解Java虚拟机原理》。

偏向锁

偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。下面我们接着了解轻量级锁

轻量级锁

倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

自旋锁

轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

锁消除

消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。

/**
 * Created by zejian on 2017/6/4.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 * 消除StringBuffer同步锁
 */
public class StringBufferRemoveSync {

    public void add(String str1, String str2) {
        //StringBuffer是线程安全,由于sb只会在append方法中使用,不可能被其他线程引用
        //因此sb属于不可能共享的资源,JVM会自动消除内部的锁
        StringBuffer sb = new StringBuffer();
        sb.append(str1).append(str2);
    }

    public static void main(String[] args) {
        StringBufferRemoveSync rmsync = new StringBufferRemoveSync();
        for (int i = 0; i < 10000000; i++) {
            rmsync.add("abc", "123");
        }
    }

}

理解:每个线程进行运行时,都会创建自己的StringBuffer变量,本身的append()方法同步是为了防止多线程同时访问进行StringBuffer对象数据的改变,造成数据不一致,所以需要将添加字符串的方法进行同步。但该代码示例中不会存在多个线程竞争StringBuffer锁的情况,它不是共享数据,只是个每个线程里的局部变量。

————————————————
版权声明:本文为CSDN博主「zejian_」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/javazejian/article/details/72828483

synchronized 特点

原子性

如果把一个事务可看作是一个程序,它要么完整的被执行,要么完全不执行。这种特性就叫原子性。

看下面代码。原子性即保证在一个线程使用同步代码时,不会有另一个线程也进入该同步代码。

public class MySynchronized implements Runnable{
    static MySynchronized instance=new MySynchronized();
    static int i=0;
    @Override
    public void run() {
        //省略其他耗时操作....
        //使用同步代码块对变量i进行同步操作,锁对象为instance
        synchronized(instance){
            for(int j=0;j<10;j++){
                System.out.println(j);
                System.out.println(Thread.currentThread().getName());
                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();
        while(t1.isAlive()) {
            System.out.println(Thread.currentThread().getName());
            System.out.println("xixi");
            System.out.println(i);
            System.out.println(t1.isAlive());
            System.out.println("haha");
        }
    }
}

运行结果:从结果可以看出线程的切换,每次的运行结果可能都不一眼,因为锁竞争也不一样。从运行结果上看,首先是运行main方法的主线程,此时因为经历过t1.start();所以t1.isAlive()==true。进入循环后运行被线程t1抢占,运行1次。i变成1,线程又被main抢占,进入循环多次后运行结束,t1线程灭亡。t2线程抢占资源进行运行。最后回到main线程,所有运行结束。

main
xixi
0
0
Thread-0
1
true
haha
main
xixi
1
true
haha

main
xixi
1
true
haha
main
xixi
1
true
haha
main
xixi
1
true
haha
main
xixi
1
true
haha
main
xixi
1
true
haha
main
xixi
1
true
haha
main
xixi
1
true
haha
main
xixi
1
true
haha
main
xixi
1
true
haha
main
xixi
1
true
haha
main
xixi
1
true
haha
main
xixi
1
true
haha
main
xixi
1
true
haha
Thread-0
2
Thread-0
3
Thread-0
4
Thread-0
5
Thread-0
6
Thread-0
7
Thread-0
8
Thread-0
9
Thread-0
0
Thread-1
1
Thread-1
2
Thread-1
3
Thread-1
4
Thread-1
5
Thread-1
6
Thread-1
7
Thread-1
8
Thread-1
9
Thread-1
main
xixi
20
false
haha

Process finished with exit code 0

虽然t2执行时,线程中断给了主线程,但synchronized保证了不会有线程同时访问同一个代码,也就是在t1执行中不会突然被线程t2抢走,但可以被不执行同步代码的线程抢走。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值