并发编程中的三个问题
可见性
可见性(Visibility):是指一个线程对共享变量进行修改,另一个线程立即得到修改后的最新值
public class Demo{
private static boolean flag=true;
public sttaic void mian(Stirng[] args){
new Thread(()->{
while(flag){
}
}).start();
Thread.sleep(2000);
new Thread(()->{
flag=false;
System.out.println("线程修改了变量的值为false");
}).start();
}
}
执行结果:
线程修改了变量的值为false
但是程序并没有结束运行 ,第一个线程任然认为flag为true
原子性
原子性(Atomicity):在一次或多次操作中,要么所有的操作都执行并且不会受其他因素干扰而中断,要么所有的操作都不执行。
public class Demo{
//定义一个共享变量number
private static int number=0;
public static void mian(String[] args)throws Exception{
//对number进行1000的++操作
Runnable increment=()->{
for(int i=0;i<1000;i++){
number++;
}
};
List<Thread> list=new ArrayList<>();
//使用5个线程来进行
for(int i=0;i<5;i++){
Thread t=new Thread(increment);
t.start();
list.add(t);
}
//确保之前的线程执行结束
for(Thread t:list){
t.join();
}
System.out.pritnln("number="+number);
}
}
执行结果:
number=4542
number++操作对应
getstatic
iconst_1
iadd
putstatic
第二个线程在第一个线程还没有把改变后的值写回变量putstatic中时就取得getstatic了原来没有改变的变量值,此时虽然两个线程都对同一个变量进行了++操作,但由于初始值都为0,最后实际的改变值只有1.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AbbIWX4C-1616408578143)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\1596963429784.png)]
有序性
有序性(Ordering):是指程序中代码的执行顺序,Java在编译和运行时会对代码进行优化,会导致程序最终的执行顺序不一定就是我们编写代码时的顺序。
并发压测:有很多线程执行下面的方法
org.openjdk.jcstress
jcstress-core
${jcstress.version}
@JCStressTest
@Outcome(id={"1","4"},expect=Expect,ACCEPTABLE,desc="ok")
@Outcome(id="0",expect=Expect.ACCEPTABLE_INTERESTING,desc="danger")
@State
public class Demo{
int num=0;
boolean ready=false;
//线程1执行的代码
@Actor
public void actor1(I_Result r){
if(ready){
r.r1=num+num;
}else{
r.r1=1;
}
}
//线程2执行的代码
@Actor
public void actor2(I_Result r){
num=2;
ready=true;
}
}
运行测试:
mvn clean install
java -jar target/jcstress.jar
结果1: 1
线程1先走,ready=false; r. r 1=1
结果2: 4
线程2先走,num =2;ready=true
再走线程1,r .r 1=num+num=4
结果3: 1
线程2中的num=2执行,然后切到线程1: r. r 1=1
结果4: 0
线程2中的ready=true执行,num=2没有执行还是0,然后线程1执行r .r 1=num+num=0
Java内存模型(JMM)
Java内容模型是Java虚拟机规范中所定义的一种内存模型,Java内存模型是标准化的,屏蔽了底层不同计算机的区别。
Java内存模型是一套规范,描述了Java程序中各种变量(线程共享变量)的访问规则 ,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。作用是多线程读写共享数据时对共享数据的可见性、有序性和原子性的规则和保障。
主内存和工作内存
主 内 存:主内存是所有线程共享的,都能访问的。所有的共享变量都存储于主内存
工作内容:每一个线程店都有自己的工作内存,工作内存只存储该线程对共享变量的副本。线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接 访问对方工作内存中的变量 。
synchronized保证三大特性
synchronized能保证在同一时刻最多只有一个线程执行该段代码,以达到保证并发安全的效果
解决原子性
public class Demo{
//定义一个共享变量number
private static int number=0;
private static Object obj=new Object();
public static void mian(String[] args)throws Exception{
//对number进行1000的++操作
Runnable increment=()->{
for(int i=0;i<1000;i++){
synchronized(obj){
number++;
}
}
};
List<Thread> list=new ArrayList<>();
//使用5个线程来进行
for(int i=0;i<5;i++){
Thread t=new Thread(increment);
t.start();
list.add(t);
}
//确保之前的线程执行结束
for(Thread t:list){
t.join();
}
System.out.pritnln("number="+number);
}
}
执行结果:
number=5000
number++操作对应
getstatic
iconst_1
iadd
putstatic
获取到锁obj对象才能进行++操作,执行完后把锁还回去。所以即使cpu把时间片分给别的线程但没有锁也无法进行++操作。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AjWXJNVr-1616408578146)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\1597029519991.png)]
解决可见性
public class Demo{
private static volatile boolean flag=true;
private static Object obj=new Object();
public sttaic void mian(Stirng[] args){
new Thread(()->{
while(flag){
sychronized(obj){
}
}
}).start();
Thread.sleep(2000);
new Thread(()->{
flag=false;
System.out.println("线程修改了变量的值为false");
}).start();
}
}
Lock原子操作就会让工作内存中变量刷新,获取最新值
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nYWkiWXN-1616408578149)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\1597030399833.png)]
有序性
为什么要重排序
为了提高程序的执行效率,如果操作之间不存在数据依赖关系,编译器和CPU会对程序中代码进行重排序
@JCStressTest
@Outcome(id={"1","4"},expect=Expect,ACCEPTABLE,desc="ok")
@Outcome(id="0",expect=Expect.ACCEPTABLE_INTERESTING,desc="danger")
@State
public class Demo{
private Object obj=new Object();
int num=0;
boolean ready=false;
//线程1执行的代码
@Actor
public void actor1(I_Result r){
synchronized(obj){
if(ready){
r.r1=num+num;
}else{
r.r1=1;
}
}
}
//线程2执行的代码
@Actor
public void actor2(I_Result r){
synchronized(obj){
num=2;
ready=true;
}
}
}
synchronized的特性
可重入特性
可重入:一个线程可以多次执行synchronized重复获取同一把锁。
public class Demo{
public static void main(String[] args){
new MyThread().start();
new MyThread().start();
}
}
class MyThead extends Thread{
@Override
public void run(){
synchronized(MyThread.class){
System.out.println(getName()+"进入了同步代码块1");
synchronized(MyThread.class){
System.out.println(getName()+"进入同步代码块2");
}
}
}
}
执行结果:
Thread-0进入了同步代码块1
Thread-0进入了同步代码块2
Thread-1进入了同步代码块1
Thread-1进入了同步代码块2
synchronized的锁对象中有一个计数器,会记录线程获得几次锁,在执行完同步代码块时,计数器的数量会-1,直到计数器的数量为0,就释放这个锁
1.可以避免死锁
2.可以更好的封装代码
不可中断特性
一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断
public class Demo{
private static Object obj=new Object();
public static void mian(String[] args){
//定义一个Runnable
Runnable run=()->{
//在Runnable定义同步代码块
synchronized(obj){
String name=Thread.currentThread().getName();
System.out.println(name+"进入同步代码块");
//保证不退出同步代码块
try{
Thread.sleep(888888);
}catch(InterruptedException e){
e.printStackTrace();
}
}
};
//先开启一个线程来执行同步代码块
Thread t1=new Thread(run);
t1.start();
Thread.sleep(1000);
//后开启一个线程来执行同步代码块(阻塞状态)
Thread t2=new Thread(run);
t2.start();
//停止第二个线程
System.out.pritnln("停止线程前");
t2.interrupt();
System.out.println("停止线程后");
System.out.println(t1.getState());
System.out.println(t2.getState());
}
}
Thread-0进入同步代码块
停止线程前
停止线程后
TIMED_WAITING
BLOCKED
Lock的不可中断和可中断
synchronized原理
public class Demo{
private static Object obj=new Object();
public static void main(String[] agrs){
synchronized(obj){
System.out.println("1");
}
}
public synchronized void test(){
System.out.println("a");
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hr1E6buu-1616408578151)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\1597401009910.png)]
每一个对象都会有一个监视器monitor关联。监视器被占用时会被锁住,其他线程无法获取该monitor,当jvm执行某个线程的某个方法内部的monotorenter时,他会尝试取获取当前对象对应的monitor的所有权
1.若monitor的进入数为0,线程可以进入monitor,并将monitor的进入数设置为1。当前线程成为monitor的owner
2.若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数+1
3.若其他线程已经占有monitor的所有权 ,那么当前尝试获取moniitor的所有权的线程会被阻塞,直到monitor的进入数变为0,才能重新尝试获取monitor的所有权。
1.能执行monitorexit指令的线程一定时拥有当前对象的monitor的所有权的线程
2.执行monitorexit时会将monitor的进入数-1.当monitor的进入数减为0时,当前线程退出monitoer,不再拥有monitor的所有权,此时其他内这个monitor阻塞的线程可以尝试去获取这个monitor的所有权
面试题:synchronized出现异常会释放锁
同步方法源码
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lmv49DYj-1616408578153)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\1597402305316.png)]
同步方法:
同步方法在反汇编后,会增加ACC_SYNCHRONIZED修饰,会隐式调用monitorenter和monitorexit。在执行同步方法前会调用monitorenter,在执行完同步方法后会调用monitorexit
面试题:synchronized和Lock的区别
1.synchronized是一个关键字,Lock是一个接口
2.synchronized会自动释放锁,Lock必须手动释放锁
3.synchronized是不可中断的,Lock可以中断也可以不中断
4.通过Lock可以知道线程有没有拿到锁,而synchronized
5.synchronized能锁住方法和代码块,而Lock只能锁代码块
6.Lock可以使用读锁提高线程的效率
7.synchronized是非公平锁,ReentrantLock可以控制它是否为公平锁。无参非公平,有参可以设置是否公平。
深入JVM源码
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dL31Jwbv-1616408578154)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\1597403559145.png)]
volatile
可见性
在多线程并发执行下,多线程修改共享的成员变量,会出现一个线程修改了共享变量的值,另一个线程不能直接看到该线程修改后的变量的最新值。
问题:
public class Demo{
public static void main(String[] args){
MyThread t=new MyThread();
t.start();
//主线程执行
while(true){
if(t.getFlage()){
System.out.println("主线程进入循环执行");
}
}
}
}
class Mythread extends Thread{
private boolean flag=false;
@Override
public void run(){
Thread.sleep(1000);
flag=true;
System.out.println("flag="+flag);
}
//getter/setter
}
执行结果:
falg=true
此时没有进入循环,子线程修改了变量,但主线程没有访问到最新值
解决:
public class Demo{
public static void main(String[] args){
MyThread t=new MyThread();
t.start();
//主线程执行
while(true){
sunchronized(t){ //在读取共享变量的线程中加锁
if(t.getFlage()){
System.out.println("主线程进入循环执行");
}
}
}
}
}
class Mythread extends Thread{
private boolean flag=false;
@Override
public void run(){
Thread.sleep(1000);
flag=true;
System.out.println("flag="+flag);
}
//getter/setter
}
public class Demo{
public static void main(String[] args){
MyThread t=new MyThread();
t.start();
//主线程执行
while(true){
if(t.getFlage()){
System.out.println("主线程进入循环执行");
}
}
}
}
class Mythread extends Thread{
private volatile boolean flag=false;
@Override
public void run(){
Thread.sleep(1000);
flag=true;
System.out.println("flag="+flag);
}
//getter/setter
}
1.子线程和主线程启动时都会拷贝一份主内存中共享变量的副本
2.子线程t修改flag的值并同步到主内存中
3.通过底层的总线机制关注主内存中的共享变量是否修改并提取最新的值
不能保证原子性
public class Demo{
public static void main(Strnig[] agrs){
Runnable target=new ThreadTarget();
for(int i=1;i<=100;i++){
new Thread(target,"第"+i+"个线程").start();
}
}
}
class ThreadTarger implements Runnable{
private volatile int count=0;
@Override
public void run(){
for(int i=1;i<=10000;i++){
count++;
System.out.println(Thread.currentThread().getName()+"count====="+count);
}
}
}
执行结果:
999652
并没有保证原子性操作
public class Demo{
public static void main(Strnig[] agrs){
Runnable target=new ThreadTarget();
for(int i=1;i<=100;i++){
new Thread(target,"第"+i+"个线程").start();
}
}
}
class ThreadTarger implements Runnable{
// private Object obj=new Object();
private volatile int count=0;
@Override
public void run(){
sunchronized(ThreadTarget.class){
for(int i=1;i<=10000;i++){
count++;
System.out.println(Thread.currentThread().getName()+"count====="+count);
}
}
}
}
1000000
使用原子类实现变量修改的原子性操作
public class Demo{
public static void main(Strnig[] agrs){
Runnable target=new ThreadTarget();
for(int i=1;i<=100;i++){
new Thread(target,"第"+i+"个线程").start();
}
}
}
class ThreadTarger implements Runnable{
//提供一个原子类
private AtomicInteger atomicInteger=new AtomicInteger();
@Override
public void run(){
for(int i=1;i<=10000;i++){
count++;
System.out.println(Thread.currentThread().getName()+"count====="
+atomicInteger.incrementAndGet());//先底层后获取
}
}
}
有序性,禁止指令重排序
public class Demo{
public static int a=0,b=0;
public static int i=0,j=0;
public static void main(String[] agrs)throws Exception{
//定义两个线程
Thread t1=new Thread(new Runnable(){
@Override
public void run(){
a=1;
i=b;
}
});
Thread t2=new Thread(new Runnable(){
@Override
public void run(){
b=1;
j=a;
}
});
t1.start();
t2.start();
t1.join(); //让t1线程优先执行完毕
t2.join(); //让t2线程优先执行完毕
System.out.pritnln("i="+i+",j="+j);
}
}
i=0,j=1
i=1,j=0 //线程b先执行
i=1,j=1
i=0,j=0 //指令重排序
public class Demo{
public volatile static int a=0,b=0;
public volatile static int i=0,j=0;
public static void main(String[] agrs)throws Exception{
//定义两个线程
Thread t1=new Thread(new Runnable(){
@Override
public void run(){
a=1;
i=b;
}
});
Thread t2=new Thread(new Runnable(){
@Override
public void run(){
b=1;
j=a;
}
});
t1.start();
t2.start();
t1.join(); //让t1线程优先执行完毕
t2.join(); //让t2线程优先执行完毕
System.out.pritnln("i="+i+",j="+j);
}
}
不会出现i=0,j=0的情况
happens-before
如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。简单来说:前一个操作的结果可以被后续的操作获取
规则
1.程序顺序规则(单线程规则)
一个线程中的每个操作,happens-before于该线程中的任意后续操作
同一个线程中前面的所有写操作对后面的操作可见
2.锁规则(Synchronized、Lock)
对一个锁的解锁,happens-before于随后对这个锁的加锁
如果线程1解锁了monitor a,接着线程2锁定了a,那么,线程1解锁a之前的写操作都对线程2可见
3.volatile变量规则
对一个volatile域的写,happens-before于任意后续对这个volatile域的读
如果线程1写入了volatile变量v(临界资源),接着线程2读取了v,那么,线程1写入v及之前的写操作都对线程2可见
4.传递性:
如果A happens-before B,且B happens-before C,那么A happens-before C
5.start()规则
如果线程A执行操作ThreadB.start(),那么A线程的ThreadB.start()操作happens-befor于线程B中的任意操作
假定线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A对共享变量的修改在接下来线程B开始执行前对线程B可见。注意:线程B启动之后,线程A在对变量修改线程B未必可见
6.join()规则:
如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回
volatile写读建立的happens-before关系研究
面试题:synchronized和Lock的区别
1.synchronized是关键字,而Lock是一个接口
2.synchronized会自动释放锁,而Lock必须手动释放锁
3.synchronized是不可中断的,Lock可以中断也可以不中断
4.通过Lock可以知道线程有没有达到锁,而synchronized不能
5.synchronized能锁住方法和代码块,而Lock只能锁住代码块
6.Lock可以使用读锁提高多线程读效率
7.synchronized是非公平锁,ReentrantLock可以控制是否是公平锁(是否为先来后到)
ReentrantLock() {sync=new NofairSync();} 无参构造是不公平锁
ReentrantLock(boolean fair) {sync=fair?new FairSync() : new NofairSync() } 指定是否为公平锁
jvm源码
monitor竞争
1.通过CAS尝试把monitor的owner字段设置为当前线程
2.如果设置之前的owner指向当前线程,说明当前线程再次进入monitor,即重入锁,执行recursions++,记录重入的次数
3.如果当前线程是第一次进入该monitor,设置recursions为1,_owner为当前线程,该线程成功获得锁并但返回
4.如果获取锁失败,则等待锁的释放。
monitor等待
1.当前线程被封装成ObjectWaiter对象node,状态设置为ObjectWaiter::TS_CXQ
2.在for循环中,通过CAS把node节点push到_cxq列表中,同一时刻可能有多个线程吧自己的node节点push到 _cxq列表中
3.node节点push到_cxq列表之后,通过自旋尝试获取锁,如果还是没有获取到锁,则通过park将当前线程挂起,等待被唤醒
4.当该线程被唤醒时,会从挂起的点继续执行,通过objecctMonitor::TryLock尝试获取锁。
monitor释放
当某个持有锁的线程执行完同步代码块时,会进行锁的释放,给其他线程机会执行同步代码块,在HostSpot中,通过退出monitor的方式实现锁的释放,并通知被阻塞的线程。
流程
1.退出同步代码块会让_recursions-1,当他的值减为0时,说明线程师范了锁。
22.根据不同的策略(由QMode指定),从cxq或EntryList中获取头节点,通过objectMonitor::ExitEpilog
方法唤醒该节点封装的线程,唤醒操作最终由unpark完成
monitor是重量级锁
ObjectMonitor的函数调用汇总会涉及到Atomic::cmpxchg_ptr,Automic:inc_ptr等内核函数,执行同步代码块,没有竞争到锁的对象会park()被挂起,竞争到锁的线程会unpark()唤醒。这个时候就会存在操作系统用户态和内核态的转换,这种切换会小航大量的系统资源。所以synchronized是重量级的操作
Linux操作系统的体系架构分为:用户空间(应用程序的活动空间)和内核
内核:本质上可以理解为一种软件,控制计算机的硬件资源,并提供上层应用程序运行的环境
用户空间:上层应用程序活动的空间。应用程序的执行必须依托于内核提供的资源,包括CPU资源。存储资源、IO资源等
系统调用:为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口,即系统调用
所有进程初始都运行于用户空间,此即为用户运行状态(用户态),但是当它调用系统调用执行某些操作时,例如I/O调用,此时需要陷入内核中运行,我们就称进程处于内核运行态(简称为内核态)。系统调用的过程可以简单理解为:
1.用户态程序 将一些数据值放在寄存器中,或者使用参数创建一个堆栈,以此表明需要操作系统提供的服务
2.用户态程序执行系统调用
3.CPU切换到内核态,并跳到位于内存指定位置的指令
4.系统调用处理器(system call handler)会读取程序放入内存的数据参数,并执行程序请求的服务
5.系统调用完成后,操作系统会重置CPU为用户态并返回系统调用的结果
由此可见用户态切换至内核态需要传递很多变量,同时内核还需要保护哈用户态在切换时的一些寄存器值、变量等,以备内核态切换回用户态。这种切换就带来了大量的系统资源消耗,这就时synchronized未优化之前,效率低的原因
Synchronized优化
CAS
CAS概述和作用
Compare And Swap(比较相同再交换)
CAS可以将比较和交换转换为原子操作,这个原子操作直接由CPU保证,CAS可以保证共享变量赋值时的原子操作。CAS操作依赖3个值:内存中的值V,旧的预估值X,要修改的新值B,如果旧的预估值X等于内存中的值V,就将新的值B保存到内存中
public class Demo{
//定义一个共享变量number
private static AtomicInteger atomicInteger=new AtomicInteger();
public static void mian(String[] args)throws Exception{
//对number进行1000的++操作
Runnable increment=()->{
for(int i=0;i<1000;i++){
atomicInteger.incrementAndGet();
}
};
List<Thread> list=new ArrayList<>();
//使用5个线程来进行
for(int i=0;i<5;i++){
Thread t=new Thread(increment);
t.start();
list.add(t);
}
//确保之前的线程执行结束
for(Thread t:list){
t.join();
}
System.out.pritnln("atomicInteger="+atomicInteger.get());
}
}
执行结果:
atomicInteger=5000
说明atomicInteger.incrementAndGet()可以保证变量赋值的原子性
CAS原理 7
AtomicInteger中有Unsafe类
Unsafe类使Java有了指针一样操作内存空间的能力,同时也带来了指正的问题。过渡的使用Unsafe类会使得出错的机率变大。
锁升级
高并发是从JDK1.5到JDK1.6的一个重要改进,HotSpot虚拟机实现了各种锁优化记住,包括偏向锁、轻量级锁和适应性自旋、锁消除、锁粗化等 这些技术都是为了在线程之间更高效地共享数据、以及解决竞争问题,从而提高程序的执行效率。
无锁->偏向锁->轻量级锁->自旋->重量级锁
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-osBCvnFa-1616408578155)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\1601426839948.png)]
Java对象的布局
锁的状态存在于Java的对象布局里
在JVM中,对象在内存中的布局分为:对象头、实例数据、对齐填充
对象头
当一个线程尝试访问synchronized修饰的代码块时,它首先要获得锁,锁在锁对象 的对象头中
HotSpot采用instanceOopDesc(普通对象的)和arrayOopDesc来描述对象头,arrayOopDesc对象用来描述数组类型,
偏向锁
在大多数情况下,锁不仅不存在多线程竞争 ,而且总是由同一线程多次获得,为了让线程获得锁的代价更低 ,引进了偏向锁。这个锁会偏向于第一个获得它的线程,会在对象头存储锁偏向的线程ID,以后该线程进入和退出同步块时只需要检查是否为偏向锁。锁标志位以及ThreadID即可
反复进入同一个进程时适合偏向锁,不过一旦出现多个线程竞争时必须撤销偏向锁所以撤销偏向锁消耗的性能必须 小于之前节省下来 的CAS原子操作的性能消耗,不然就得不偿失了
public class Demo{
public static void main(String[] agrs){
MyThread mt=new MyThread();
mt.start();
}
}
class MyThread extends Thread{
static Object obj=new Object();
@Override
public void run(){
for(int i=0;i<5;i++){
synchronized(obj){
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
}
}
偏向锁在1.6之后时默认启用的,但在应用程序 启动几秒之后才激活,可以使用JVM参数关闭延迟,如果确定应用程序中所有锁通常情况下处于竞争状态,可以通过jvm参数关闭偏向锁
偏向锁原理
当线程第一次访问同步块并获取锁时,偏向锁处理流程如下:
1.虚拟机将会把对象头中的标志位设为01,即偏向模式
2.同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Work之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高。
1.检测Mark Word是否为可偏向状态,即是否为偏向锁1,锁标识位位01
2.若位可偏向状态,则测试线程ID是否为当前线程ID,如果是 ,执行同步代码块,否则执行步骤3
3.如果测试线程ID不为当前线程ID,则通过CAS操作将Mark Word的线程ID替换为当前线程 ID,执行同步代码块
偏向锁的撤销
1偏向锁的撤销动作 必须等待全局安全点(这个点所有的线程都被停下来了)
2.暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态
3.撤销偏向锁,恢复到无锁(标志位为01)或轻量级锁(标志位位00)的状态
偏向锁好处
偏向锁是在只有一个线程执行同步块时进一步提高性能,适用于一个线程反复获得同一锁的情况,偏向锁可以提高带有同步但无竞争的程序性能。
它同样是 一个带有效益权衡性质的优化,也就是说,它并不一定总是对程序运行有利,如果程序中大多数的锁总是被多个不同的线程访问,比如线程池,那偏向模式就是多余的
轻量级锁
引入轻量级锁的目的:在多线程交替执行同步块的情况下,尽量避免重量级锁引起的性能消耗,但如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁,所以轻量级锁的出现并非是要替代重量级锁
轻量级锁的原理
当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级位重量级锁,则会尝试获取轻量级锁:
1.判断当前对象是否处于无序状态(hashcode、0、01),如果是,则JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对目前的Mark Work的拷贝,将Lock Record中的owner指向当前对象
2.JVM利用CAS操作 尝试将对象的Mark Word更新为指向Lock Record的指针,如果成功表示竞争到锁,将锁标志位变为00(轻量级锁),执行同步操作
3.如果失败则判断当前对象的Mark Word是否指向当前线程的栈帧 ,如果是则表示当前线程已经持有当前对象的锁 ,则直接执行同步代码块,否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级 锁,锁标志位变为10,后面等待的线程将会进入阻塞状态。
栈帧:一个进入栈中的方法,就是一个栈帧
轻量级锁的释放
轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下:
1.取出在获取轻量级锁保存在Displaced Mark Word中的数据
2.用CAS操作将取出的数据替换当前对象的Mark Word中,如果成功,则说明释放锁成功
3.如果CAS 操作替换失败,说明有其他线程尝试获取该锁,则需要将轻量级搜索需要 鞥张伟重量级锁
对于轻量级锁,其性能提升的依据是“对于绝大部分的锁,在整个生命周期内都不会存在竞争的”,如果打破这个依据则除了互斥的开销外,还有额外的CAS操作,因此在有多线程竞争的情况下,轻量级锁闭重量级锁更慢
自旋锁
monitor实现锁的时候,知道monitor会阻塞和唤醒线程,线程的阻塞和唤醒需要CPU从用户态桩位核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,这些操作给系统的并发性能带来了很大的压力。而共享数据的锁定状态只会持续很短的一段时间,为了这段时间阻塞和唤醒线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行 ,我们就可以让后面请求锁的那个线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这就是所谓的自旋锁
锁消除
锁消除是指虚拟机即时编译器(JIT)在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把他们当做栈上数据对待,认为他们是线程私有的,同步加锁自然就无须进行。
public class Demo{
public static void main(String[] args){
contactString("aa","bb","cc");
}
public static String contactString(String s1,String s2,String s3){
return new StringBuffer().append(s1).append(s2).append(s3).toString();
}
}
append方法时synchronized同步方法,理论 上来说调用三次会执行三次同步 方法,由于普通同步方法使用的锁就是this,在这里也就是new StringBuffer()(这是一个方法内部的局部变量,没有逃逸出这个contactString方法,就算有多线程进入这个 方法拿到的也是不同的锁),实际上是不存在竞争的,同步synchronized就没有必要了,即时编译器(JIT)知道这里没有竞争就会消除掉synchronized
锁粗化
原则上,我们在 编写代码的时候,总是推荐 将同步块的作用范围限制得尽量小,这样偏向锁或者轻量级锁、自旋就有更大的可能满足要求,值在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。大部分情况下,上面的原则都是正确的,但是如果一系列的连续操作都堆同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即时没有线程竞争,频繁的进行互斥同步操作也会导致不必要的性能损耗。
JVM会探测到一连串细小的操作都使同一个对象加锁,将同步代码块的范围 方法,放到这串操作的外面 ,这样值需要加一次锁即可。
public class Demo{
public static void mian(String[] args){
StringBuffer sb=new StringBuffer();
//synchronized{
for(int i=0;i<100;i++){
sb.append("aa");
}
//}
System.out.println(sb.toString());
}
}
在循环中调用append方法代表进入同步代码块100次,出同步代码块100次,性能消耗大,这里就会把append方法中的synchronized抹掉,加到循环外面。
总结
减少synchronized的范围
降低synchronized锁的粒度
将一个锁才分成多个锁提高高并发
hashtable
看源码可是,put、get、remove方法上都加了 synchronized锁,这里用的锁是this,意味着hashtable的增删改查方法都变成了同步方法,hashtable这个对象,如果有一个线程put,另一个线程必须等这个线程添加完才可以增删改查,但实际上这样是没有必要的,因为是在不同的Entry中添加。效率就低
consurrentHashMap
查看源码可知,get方法没有锁,put上没有锁,但代码里面调用的方法添加了具有不同锁对象的同步方法以实现修改哪个Entry就锁住哪一个Entry。
读写分离
读取不加锁,写入和删除时加锁
concurrentHashMap
CountDownLatch
类似于锁,但它是一次性挂若干个锁,一个一个往下解,解没了以后就可以正常运行了
可以和锁混合使用,或替代锁的功能,在门栓未完全开放之前等待,完全开放后执行,避免锁的效率低下问题。
import java.util.concurrent.CountDownLatch;
public class Test{
CountDownLatch latch=new CountDownLatch(5); //一个门栓上5把锁
void m1(){
try{
latch.await(); //等待门栓开放
}catch(InterruptedException e){
e.prinrStackTrace();
}
System.out.println("m1() method");
}
void m2(){
for(int i=0;i<10;i++){
if(latch.getCount()!=0){ //判断门栓上还有几把锁
System.out.println("latch count:"+latch.getCount());
lacth.countDown(); //减门栓上的锁
}
try{
TimeUnit.MILLISECONDS.sleep(500);
}catch(InterriptedException e){
e.printStackTrace();
}
System.out.println("m2() method:"+i);
}
}
public static void main(String[] args){
final Test=new Test();
new Thread(new Runnable(){
@Override
public void run(){
t.m1();
}
}).start();
new Thread(new Runnable(){
@Override
public void run(){
t.m2();
}
}).start();
}
}
并发编程 reentrantLock
reentrantLock是重入锁,重入锁是支持重入性,表示能够对共享资源重复加锁,即当前线程获取该锁再次获取不会被阻塞
public class Test{
private final LinkedList<E> list=new LinkedList<>();
private final int MAX=10;
private int count=0;
private Lock lock=new ReentrantLock();
private Condition producer=lock.newCondition();
private Condition consumer=lock.newCondition();
public int getCount(){
return count;
}
public void put(E e){
lock.lock();
try{
while(list.size()==MAX){
}
}
}
}
AQS(AbstractQueuedSysnchronizer)
AQS是一个用来构建锁和同步器的框架,如ReentrantLock等
AQS核心思想是,如果被请求的共享资源空闲,就讲当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的贡献资源被占用,name就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-62A1dJNZ-1616408578156)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\1611825151857.png)]
AQS定义两种资源共享方式:
Exclusive(独占):只有一个线程能执行,如ReentrantLock。有可分为公屏上和非公平锁
Share(共享):多个线程可同时执行。ReadWriteLock
并发编程的缺点
内存泄露、上下文切换、线程安全、死锁
线程的状态
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aY1Vb4Ix-1616408578157)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\1600066229283.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WYsGB27H-1616408578158)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\1600066496185.png)]
1.new 尚未启动的线程处于此状态
2.Runnable 在java虚拟机中执行的线程处于此状态 (就绪、执行)
3.Blocked 被阻塞等待监视器锁定的线程
4.waiting 正在等待另一个线程执行特定动作的线程处于此状态
5.TIMED_WAITING 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
6.TERMINATED 死亡状态
public class AllState{
public static void mian(String[] args){
Thread t=new Thread(()->{
for(int i=0;i<5;i++){
try{
Thread.sleep(100);
}catch(InterruptedException e){
e.printStackTrace();
}
}
System.out.println("....");
});
//观察状态
State state=t.getState(); //NEW
t.start();
State state=t.getState(); //RUNNABLE
while(state!=Thread.State.TERMINATED){ //监控状态
try{
Thread.sleep(200);
}catch(InterruptedException e){
e.printStackTrace();
}
state=t.getState(); //TIMED_WAITING
}
state=t.getState(); //TERMINATED
}
}
就绪状态发生的原因**
1.start
2.解除阻塞状态
3.yield
4.jvm
阻塞状态发生的原因
1.sleep、join发出I/O请求时,线程会进入阻塞状态。当sleep状态超时、join等待线程终止或I/O处理完毕时重新进入就绪状态
2.同步阻塞:线程在获取synchronized同步锁失败,则jvm会把该线程放入锁池,即进入同步阻塞状态
3.wait:等待阻塞,wait方法执行时,jvm会把该线程放入等待队列,使线程进入阻塞状态,并释放锁
线程方法
**sleep()**不释放锁
sleep(时间)指定当前线程阻塞的毫秒数
sleep存在异常InterruptedException
使线程停止运行一段时间,将处于阻塞状态,时间达到后线程进入就绪状态
如果调用了sleep方法之后,没有其他等待执行的线程,这个时候当前线程不会马上恢复执行
pubic class BlockedSleep{
public static void mian(String[] args){
Web12306 web=new Web12306();
System.out.println(Thread.currentThread().getName());
new Thread(web,"mm").start();
new Thread(web,"nn").start();
new Thread(web,"pp").start();
}
}
class Web12306 implements Runnable{
private int ticketNums=99;
public void run(){
while(true){
if(ticketNums<0){
break;
}
try{
Thread.sleep(200);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentTread().getName()+"-->"+ticketNums--);
}
}
}
public class BlockedSleep03{
public static void main(String[] args)throws InterruptedException{
//倒计时
Date endTime=new Date(System.currentTimeMillis()+1000*10); //10秒
long end=endTime.getTime();
while(true){
System.out.println(new SimpleDateFormat("mm:ss").format(endTime));
Thread.sleep(1000);
endTime=new Date(endTime.getTime()-1000);
if(end-10000>endTime.getTime()){
break;
}
}
}
public static void test()throws InterruptedException{
//倒数10个数,1秒1个
int num=10;
while(true){
Thread.sleep(1000);
System.out.println(num--);
}
}
}
join() 插队
阻塞指定线程(阻塞)等到另一个线程完成以后再继续执行
待此线程执行完成后,在执行其他线程,将其他线程 阻塞
public class demo{
public static void main(String[] args)throws InterruptedException{
Thread t=new Thread(()->{
for(int i=0;i<100;i++){
System.out.println("lambda..."+i);
}
});
t.satrt();
for(int i=0;i<100;i++){
if(i==20){
t.join(); //插队 main被阻塞了
}
}
}
}
开始是main方法走直到main…19的时候,main线程被lambda线程插队了,谁调用join方法,谁去插队,插队的方法写在哪里,哪个线程就被插队。插队的线程必须执行完成,才能继续执行原来的线程
//爸爸想抽烟,让儿子去买烟,儿子买回烟来才能抽,这相当于一个插队方法
public class demo{
puublic static void main(String[] args){
System.out.println("爸爸和儿子买烟的故事");
new Thread(new Father()).start();
}
}
class Fatehr extends Thread{
public void run(){
System.out.println("想抽烟,发现没了");
System.out.println("让儿子买烟");
Thread t=new Thread(new Son());
t.start();
try{
t.join(); //father被阻塞
}catch(InterruptedException e){
e.printStackTrace();
System.out.println("孩子走丢了,老爸出去找孩子");
}
System.out.println("可以抽烟了");
}
}
calss Son extends Thread{
public void run(){
System.out.prnitnln("去买烟");
System.out.println("停10s");
for(int i=0;i<10;i++){
System.out.println(i+"秒过去了");
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("买烟回家");
}
}
}
…
9秒过去了…
买烟回家
可以抽烟了
yield()
让当前正在执行的线程暂停,不是阻塞线程,而是将线程转入就绪状态
调用了yield方法之后,如果没有其他等待执行的线程,此时当前线程就会马上恢复执行
//两个线程在执行过程中礼让
public class demo{
public static void mian(String[] args){
MyYield my=new MyYield();
new Thread(my,"a").start();
new Thread(my,"b").start();
}
}
class MyYield implements Runnable{
@Override
public void run(){
System.out.println(Thread.currentThread().getName()+"-->start");
Thread.yield(); //礼让
System.out.println(Thread.currentThread().getName()+"-->end");
}
}
礼让成功的结果:
b–start a–>start b–end a–end
礼让不成功的结果:
b–>satrt b–end a–satrt a–end
//在主线程中进行礼让
public calss demo{
public static void main(String[] args){
new Thread(()->{
fo(int i=0;i<100;i++){
System.ut.println("lambda..."+i);
}
}).satrt();
for(int i=0;i<100;i++){
if(i%20==0){
Thread.yield(); //main礼让
}
System.out.println("main..."+i);
}
}
}
当main…20之后出现的不是 main…21,而是lambda…i的时候说明礼让了
setDaemon()
可以将指定的线程设置为后台线程,守护线程
创建用户线程的线程结束时,后台线程也随之消亡
只能在线程启动之前把他设为后台线程
虚拟机停止必须确保用户线程执行完毕 虚拟机停止不用等待守护线程执行完毕
如后台记录操作日志,监控内存使用等
public class DeamonTest{
public static void main(String[] args){
God god=new God();
You you=new You();
Thread t=new Thread(god);
t.setDeamon(true); //将用户线程调整为守护
t.start();
new Thread(you).satrt();
}
}
class You implements Runnable{
@Override
public void run(){
for(int i=1;i<=365*100;i++){
System.out.println("happy..life");
}
System.out.println("oooo..");
}
}
calss God implements Runnable{
@Override
public void run(){
Systemout.pritln("bless you...");
}
}
God为用户线程的时候是无限循环的
God为守护线程的时候虽然他设置为无限循环,但在You这个用户线程结束后,虚拟机就会结束
void setProproty(int newPriority) int getPriority()
线程的优先级代表的时概率
优先级的设定建议在start()调用之前
范围从1到10,默认为5
Thread.MIN_PRIORITY=1
Thread.MAX_PRIORITY=10
Thread.NORM_PRIORITY=5
public class PriorityTest{
public static void main(String[] args){
System.out.println(Thread.currentThread().getPriority());
MyPriority mp=new MyPriority();
Thread t1=new Thread(mp);
Thread t2=new Thread(mp);
Thread t3=new Thread(mp);
Thread t4=new Thread(mp);
Thread t5=new Thread(mp);
Thread t6=new Thread(mp);
//设置优先级在启动之前
t1.setPriority(Thread.MAX_PROPRITY);
t2.setPriority(Thread.MIN_PROPRITY);
t3.setPriority(Thread.NORM_PROPRITY);
t4.setPriority(Thread.MAX_PROPRITY);
t5.setPriority(Thread.MIN_PROPRITY);
t6.setPriority(Thread.NORM_PROPRITY);
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
t6.start();
}
}
class MyPrioruty implements Runnable{
@Override
public void run(){
System.out.println(Thread.currentThread().getName()+
"-->"+Thread.currentThread().getPriority());
}
}
优先级高的线程有更大的可能先执行,但不绝对
stop()
停止线程,不推荐使用,应该让他自己执行完
public class TerminateThread implements Runnable{
privatee boolean flag=true;
private String name;
public TerminateThread(String name){
this.name=name;
}
public void run(){
int i=0;
while(flag){
System.out.println(name+"-->"+i++);
}
}
//对外提供方法改变标识
public void terminate(){
this.flag=false;
}
public static void main(SString[] args){
TerminateThread tt=new TerminateThread("xx");
new Thread(tt).start();
for(int i=0;i<=99;i++){
if(i==88){
tt.terminate(); //线程终止
System.out.println("game over");
}
System.out.println("mian-->"+i);
}
}
}
面试题
sleep与wait的区别
1.对于sleep()方法,是属于Thread类中的。而wait()方法,则是属于Object类中的
2.sleep()方法导致了程序暂停执行指定的时间,让出CPU给其他线程,但是他的监控状态依然保持着,当指定时间到了又会自动恢复运行状态
3.在调用sleep()方法的过程中,线程不会释放对象锁
4.当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入 对象锁定池准备获取对象锁进入运行状态
stop()和suspend()方法为何不推荐使用
stop线程不安全,它会解除由线程获取的所有锁定,而且如果对象处于一种不连贯状态,那么其他线程能在那种状态下检查和修改它们。结果很难检查出真正的问题
suspend()方法容易发送死锁。调用suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。此时,其他任何线程都不能访问锁定的资源,除非被“挂起”的线程恢复运行
同步和异步的区别
如果数据将在线程间共享。例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,必须进行同步存取。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率
当一个线程进入一个对象的一个synchronized方法后,其他线程是否可进入此对象的其他方法
其他方法前是否加了synchronized关键字,没加则可以
如果这个方法内部调用了wait,则可以进入其他synchronized
如果其他方法都加了synchronized关键字 ,并且内部没有调用wait,则不能
如果其他方法时static,它用的同步锁就时当前类的字节码,与非静态的方法不能同步,营维非静态的方法用的是this
常用的其他方法
isAlive() :线程是否还活着
Thread.currentThread():当前线程
setName() /getName() 代理对象
public class InfoTest{
public static void mian(String[] args)throws InterruptedException{
System.out.println(Thread.currentThread().isAlive()); //true
//设置名称:真实角色+代理角色
MyInfo info=new MyInfo("战斗机");
Thread t=new Thhread(info);
t.setName("公鸡");
t.start();
Thread.sleep(1000);
System.out.println(t.isAlive()); //false
}
}
class MyInfo implements Runnable{
private String name;
public MyInfo(String name){
supper();
this.name=name;
}
@Override
public void run(){
//公鸡-->战斗机
System.out.println(Thread.currentThread().getName()+"-->"+name);
}
}
true
公鸡–>战斗机
false
并发编程的要素
- 原子性:即一个不可再被分割的颗粒。指一个或多个操作要么全部执行成功,要么全部失败。
- 可见性:一个线程对共享变量的修改,另一个线程能立即看到。
- 有序性:程序执行的顺序按照代码的先后顺序执行
出现线程安全的原因: 解决办法:
1.线程切换带来的原子性问题 1.JDK Atomic开头的原子类、synchronized、LOCK、可以解决原子性问题
2.缓存导致的可见性问题 2.sychronized、volatile、LOCK,可以解决可见性问题
3.编译优化带来的有序性问题 3.Happens—Before规则可以解决有序性问题
并行和并发
-
并发:多个任务在同一个CPU核上,按细分的时间片轮流执行,从逻辑上看那些任务是同时执行。
两个队列和一台咖啡机
-
并行:单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的"同时进行"。
两个队列和两台咖啡机
-
串行:有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全的情况,也就不存在临界值的问题。
一个队列和一台咖啡机
进程和线程的区别
根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的。
上下文切换
时间片轮转:当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。当前任务在执行完CPU时间片切换到另一个任务之前会保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。Linux相比于其他操作系统在上下文切换和模式切换的时间消耗很少。
守护线程和用户线程
- 用户线程:运行在前台,执行具体的任务,如程序的主线程(main)、连接网络的子线程等都是用户线程
- 守护线程:运行在后台,为其他前台线程服务,一旦所有用户线程(垃圾回收线程)都结束运行,守护线程会随JVM一起结束工作。
设置为守护线程: public final void setDDaemon(boolean on);
判断是否是守护线程: public final boolean isDaemon();
public class ThreadDemo{
public static boolean flag=true;
public static void mian(String[] args){
Threadh userThread=new Thread(()->{
for(int x=0;x<10;x++){
try{
Thread.sleep(100);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentTread().getName()+"正在运行.x="+x);
}
},"用户线程"); //完成核心业务
Thread daemonThread=new Thread(()->{
for(int x=0;x<Integer.MAX_VALUE;X++){
try{
Thread.sleep(100);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentTread().getName()+"正在运行.x="+x);
}
},"守护线程");
daemonThread.setDaemon(true); //设置为守护线程必须在start()前执行
userThread.start();
daemonThread.start();
}
}
注意事项:
1.在守护线程中产生的新线程也是守护线程。
2.不是所有的任务都可以分配给守护线程来执行,比如读写操作或计算逻辑
3.守护线程中不能依靠finally快的内容来确保执行关闭或清理资源的逻辑。主线程结束守护线程就会结束,守护线程中的finally块可能无法执行。
线程死锁
死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或由于彼此通信而造成的一种堵塞的现象,若无外力作用,它们将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)。
形成死锁的四个必要条件
互斥条件:线程对于所分配到的资源具有排他性,即一个资源只能被一个线程占用,知道被改线程释放
请求与保持条件:一个线程因请求被占用资源而发生阻塞时,对已获得的资源保持不放
不剥夺条件
循环等待条件
线程池
常用方法
ThreadLocal() 创建ThreadLocal对象
public void set(T value) 设置当前线程绑定的局部变量表
public T get() 获取当前线程绑定的局部变量
public void remove() 移除当前线程绑定的局部变量
//需求:线程隔离
//在多线程并发的场景下,每个线程中的变量都是相互独立的
//线程A: 设置(变量1) 获取(变量1)
//线程B 设置(变量2) 获取(变量2)
public class Demo{
private String content;
private String getContent(){
return content;
}
private void setContent(String content){
this.content=content;
}
public static void main(String[] args){
Demo demo=new Demo();
for(int i=-;i<3;i++){
Thread thread=new Thread(new Runnable(){
@Override
public void run(){
//每个线程:存一个变量,过一会取出这个变量
demo.setContent(Thread.currentThread().getName()+"的数据");
System.out.pritln("======");
System.out.println
(Thread.currentThread().getName()+"--->"+demo.getContent);
}
});
thread.setName("线程"+i);
thread.start();
}
}
}
执行结果:线程不隔离
线程0—>线程2的数据
=============
线程1---->线程1的数据
==============
线程2---->线程1的数据
解决:把变量绑定到线程上
public class Demo{
ThreadLocal<String> t1=new ThreadLocl<>();
private String contentt;
private String getContent(){
String s=t1.get();
return s;
}
private void setContent(String content){
t1.set(content);
}
public static void main(String[] args){
Demo demo=new Demo();
for(int i=-;i<3;i++){
Thread thread=new Thread(new Runnable(){
@Override
public void run(){
//每个线程:存一个变量,过一会取出这个变量
demo.setContent(Thread.currentThread().getName()+"的数据");
System.out.pritln("======");
System.out.println
(Thread.currentThread().getName()+"--->"+demo.getContent);
}
});
thread.setName("线程"+i);
thread.start();
}
}
}
例子
龟兔赛跑
public class Recer implements Runnable{
private String winner;
@Override
public void run(){
for(int steps=1;steps<=100;steps++){
//模拟休息
if(Thread.currentThread().getName().equals("rabbit")&&steps%10==0){
try{
Thread.sleep(100);
}catch(InterruptedException e){
e.printlnStackTrace();
}
}
System.out.pritnln(Thread.currentThread().getName()+"-->"+steps);
boolean flag=gameOver(steps);
if(flage){
break;
}
}
}
private boolean gameOver(int steps){
if(winner!=null){
return true;
}else{
if(steps==100){
winner=Thread.currentThread().getName();
System.out.println("winner is"+winner);
}
}
return false;
}
public static void main(String[]agrs){
Recer recer=new Recer();
new Thread(recer,"tortoise").start();
new Thread(recer,"rabbit").start;
}
}
影院抢票
public class Demo{
public static void main(String[] args){
Cinema c=new Cinema(20,"happy sxt");
new Thread(new Customer(c,2),"老高").satrt();
new Thread(new Customer(c,1),"老裴").start();
}
}
//顾客
class Customr implements Runnable{
Cinema cinema;
int seats;
public Customer(Cinema cinema,int seats){
this.cinema=cinema;
this.seats=seats;
}
@Overide
public void run(){
synchronized(cinema){
boolean flag=cinema.bookTickets(seats);
if(flag){
System.out.println("出票成功"+Thread.currentThread().getName()+"-->位置为:"+seats);
}else{
System.out.println("出票失败"+Thread.currentThread().getName()+"-->位置不够");
}
}
}
}
//影院
class Cinema{
int available; //可用位置
Strint name;
public Cinema(int avaulable,String name){
this.available=available;
this.name=name;
}
public boolean bookTickets(int seats){
System.out.println("可用位置为:"+available);
if(seats>available){
return false;
}
available-=seats;
return true;
}
}
执行结果:
可用位置为:20
出票成功老高–>位置为:2
可用位置为:18
出票成功老裴–>位置为:1
System.out.pritnln(Thread.currentThread().getName()+"–>"+steps);
boolean flag=gameOver(steps);
if(flage){
break;
}
}
}
private boolean gameOver(int steps){
if(winner!=null){
return true;
}else{
if(steps==100){
winner=Thread.currentThread().getName();
System.out.println(“winner is”+winner);
}
}
return false;
}
public static void main(String[]agrs){
Recer recer=new Recer();
new Thread(recer,“tortoise”).start();
new Thread(recer,“rabbit”).start;
}
}
影院抢票
```java
public class Demo{
public static void main(String[] args){
Cinema c=new Cinema(20,"happy sxt");
new Thread(new Customer(c,2),"老高").satrt();
new Thread(new Customer(c,1),"老裴").start();
}
}
//顾客
class Customr implements Runnable{
Cinema cinema;
int seats;
public Customer(Cinema cinema,int seats){
this.cinema=cinema;
this.seats=seats;
}
@Overide
public void run(){
synchronized(cinema){
boolean flag=cinema.bookTickets(seats);
if(flag){
System.out.println("出票成功"+Thread.currentThread().getName()+"-->位置为:"+seats);
}else{
System.out.println("出票失败"+Thread.currentThread().getName()+"-->位置不够");
}
}
}
}
//影院
class Cinema{
int available; //可用位置
Strint name;
public Cinema(int avaulable,String name){
this.available=available;
this.name=name;
}
public boolean bookTickets(int seats){
System.out.println("可用位置为:"+available);
if(seats>available){
return false;
}
available-=seats;
return true;
}
}
执行结果:
可用位置为:20
出票成功老高–>位置为:2
可用位置为:18
出票成功老裴–>位置为:1