1、程序、进程和线程的区别
进程和线程的关系
一个进程中可以有多个线程,多个线程共享进程的堆和方法区资源。但是每个线程有自己的程序计数器、虚拟机栈和本地方法栈。(线程私有)
为什么要使用多线程
使用多线程可能带来问题
并发编程的目的是为了能提高程序的执行效率提高程序的运行速度,但是并发编程并不总是能提高程序运行速度,而且会遇到很多问题:内存泄露、死锁、线程不安全。
2、线程的状态和生命周期
新建、就绪、运行、阻塞、死亡
(阻塞状态只能到就绪状态,而不能直接到运行)
Java使用Thread类及其子类表示线程,新建的线程在它的一个完整生命周期中通常要经历五种状态。
3、为什么程序计数器是私有的?
4、并发和并行
5、线程安全和线程不安全
1、示例代码
public class Mathine extends Thread{
public void run(){
for ( int a=0;a<10;a++){
System.out.println(currentThread().getName()+":"+a);
try {
//线程睡眠,进入阻塞状态,主动放弃处理机
Thread.sleep(100);
}catch (InterruptedException e) {throw new RuntimeException(e);
}
}
}
public static void main(String[] args) {
//线程的新建。和普通对象一样,在栈里占一块空间
Mathine mathine1 = new Mathine();
Mathine mathine2 = new Mathine();
mathine1.setName("m1");//设置线程的名称
mathine2.setName("m2");
mathine1.setPriority(Thread.MAX_PRIORITY);//设置线程的优先级
mathine2.setPriority(Thread.MIN_PRIORITY);
System.out.println("Priority of m1:"+mathine1.getPriority());
System.out.println("Priority of m2:"+mathine2.getPriority());
//线程处于就绪状态,可以有多个。就绪需要资源:指令计数器和栈空间。随机被处理机调用
mathine1.start();
mathine2.start();
//通过mathine1对象调用run方法
mathine1.run();
}
}
Mathine mathine1 = new Mathine();
Mathine mathine2 = new Mathine();
创建Methine类的对象—mathine1,mathine2,这两个对象是线程对象,对于线程来说,此时是两个线程处于新建状态。
mathine1.start();
mathine2.start();
使线程1和线程2处于就绪状态,一旦处于就绪状态,就要分配资源。虚拟机需要为这两个线程分配资源:
此时两个线程处于就绪状态,根据处理机的调度,随机占领处理机,mathine1线程占领处理机,就会执行线程对象入口方法也就是的run方法,在mathine1线程run方法栈帧中开辟空间
currentThread()
显示当前线程的地址
Thread.sleep(100);
线程睡眠,进入阻塞状态,主动放弃处理机。不会释放资源,资源保存在寄存器中。
mathine1.run();
实例方法通过对象调用,主线程通过对象调用run方法。在主线程的run方法栈帧里开辟空间。
主线程主方法执行完不一定程序结束,因为可能还有其他线程在执行方法。
程序结束,线程进入死亡状态:释放所有资源。
2、可以直接调用Thread类的run方法吗
当new一个Thread时,线程进入新建状态,调用start()方法,线程会进入就绪状态,然后等待处理机分配时间片,当分配到时间片就可以运行,线程会自动调用自己的run方法,这才是多线程工作;
如果直接调用run()方法,会把run()方法当做main线程下的一个普通方法去执行,并不会在某个线程中执行,这不是多线程工作。
1、线程命名、设置优先级
给线程设定属性只能在创建状态
mathine1.setName("m1");//设置线程的名称
mathine2.setName("m2");
mathine1.setPriority(Thread.MAX_PRIORITY);//设置线程的优先级
mathine2.setPriority(Thread.MIN_PRIORITY);
理论上,Java上层根据线程优先级调度 ,优先级高的先执行,同一优先级则随机调度。但是实际情况中,因为Java线程调度还要依赖于底层 ,所以不同系统甚至不同次运行得到的结果都有可能不相同,Java中的线程调度是非常混乱的,所以设置线程的优先级不代表优先级高的一定被优先执行。
2、Java线程调度策略
实例代码
/**
*演示线程调度策略为时间片和轮转--JavaDoc
*演示设置后台线程的方法和运行特点
*演示暂停当前线程的方法yield()
@author admin
*@version 1.0
*/
public class MultiThread1
{
public static void main(String[] args)
{
MyThread mt=new MyThread();
//设定mt线程为后台线程,理论上前台线程结束后台线程随即终止,
mt.setDaemon(true);
mt.start();
int index=0;
while(true)
{
if(index++==10)break;
System.out.println("main:"+Thread.currentThread().getName()+" "+index);
}
}
}
class MyThread extends Thread
{
public void run()
{
while(true)
{System.out.println(getName());
yield();//暂停当前线程。从运行状态到就绪状态,让主线程执行。
}
}
}
理论上前台线程结束后台线程随即终止 。那么,主线程运行while循环里的代码,一直到idnex==10,退出循环,线程结束,后台线程也会随机终止,那么后台线程理论上完全不能运行,但实际情况是这样吗?并不是,得到的结果是,后台线程也有运行。说明java线程调度策略为时间片轮转 ,并不会一直让某一个线程运行,会分出时间片给后台程序,并且时间片并不是等时分配。
后台线程时间片多,执行yield(),让后台线程从运行状态到就绪状态,让步给主线程执行。但就绪状态依然随时会占用处理机,意义不大。
4、内部类对象对外部类的实例变量的争用
实例代码
package test;
/**
*演示利用内部类实现多线程对同一个实例变量的访问
*/
public class MultiThread3
{
public static void main(String[] args)
{
//创建一个外部类对象
MyThread mt=new MyThread();
//创建新线程,调用start()方法
//创建四个外部类对象的内部类对象,四个线程对象都执行自己的run方法,四个内部类的线程对象使用同一个index
//四个线程对象争用同一个index实例变量,每次看到的index都不一样,可能一个线程看到的是1,另一个线程看到的9
mt.getThread().start();
mt.getThread().start();
mt.getThread().start();
mt.getThread().start();
}
}
class MyThread
{
int index=0;
/***************************************/
//私有的内部类,只能在外部类的方法里才能创建内部类对象
private class InnerThread extends Thread
{
public void run()
{
//在外部类中找index,不能写成this.index,这代表在内部类中找
while(index<=10)
{
System.out.println(Thread.currentThread().getName()+":"+index);
index=index+1;
}
}
}
/***************************************/
Thread getThread()
{
//this所指外部类对象的内部类对象,创建内部类对象
//内部类对象拥有所指外部类对象的引用
return this.new InnerThread();
}
}
MyThread类里有一个内部类InnerThread,是私有内部类,私有内部类的对象只能在外部类的方法里才能创建,只有外部类的方法才能识别。
外部方法getThread(),
this.new InnerThread()
创建this所指外部类对象的内部类对象。new InnerThread()是创建一个内部类对象,而这个内部类对象是线程对象,此时就是线程的新建状态 。内部类对象拥有this所指外部类对象的全部引用。
mt.getThread().start();
通过外部类对象调用外部方法来创建一个线程对象,并调用start方法,线程进入就绪状态,有指令计数器和开辟了栈空间。
线程处于就绪状态,占领处理机的线程(即运行)会执行自己线程的内部类对象的run方法,index+1,时间片到了,就会把处理机让步下一个线程。理论上,index应该从0一直加到10,但是运行结果却并不是这样,这是因为线程的争用资源 。解释如下:
创建了四个线程对象,四个线程对象访问的是同一个index变量,这个时候就出现线程对实例变量争用。例如当第一个线程运行,执行内部类的run方法,找外部类的index变量,看到index是0,此时时间片到了(还没来得及执行index++的操作),下一个线程运行,执行内部类的run方法,同样找这个相同的index变量,看到的还是0,…以此类推,而当再次轮到第一个线程执行,此时index可能已经加到了5,但是第一个线程还在执行上一次的方法,认为index是0,于是index+1,线程所看到的值和实际的值不一致,最后就导致index并不是依次增加,很混乱 。----线程不安全的
解决方案:加锁。让线程看到的index值和实际值要一样。在线程的方法执行完前加锁,即便时间片到了也不解锁,一直到执行完。
1、方式一就是前面代码那种通过一个类继承Thread类,创建这个类的对象就是线程对象。
2、方式二,实现Runnable接口
public class ThreadTest1 {
public static void main(String args[]) {
//创建一个实现了Runnable的类的对象mt
MyThread1 mt = new MyThread1(); //产生一个Runnable对象mt
//创建线程对象,线程新建
Thread t = new Thread(mt); //以Runnable对象构造一个Thread类的实例
//启动线程,线程进入就绪状态
t.start();
//执行到里,主方法结束,主线程结束,程序还没结束,处理机给线程t执行,线程t执行MyThread对象的run方法
//t线程占有处理机,执行mt所指对象的run方法
}
}
class MyThread1 implements Runnable {
int i=980;
public void run() {
while (true) { //this所指对象mt的i,也就是MyThread1里的i
System.out.println("Hello " + this.i++);
if (this.i == 1000) break;
}
}
}
1、代码如下
class ThreadDemo implements Runnable{
private boolean flag = true;
public void stopRunning(){
this.flag = false;
}
public void run(){//this所指对象tt的flag
System.out.println("falg = " + this.flag);
while(this.flag){
try{
Thread.currentThread().sleep(1000);
}
catch(InterruptedException e){
System.out.println("线程被终止");
return;
}
System.out.println("I love you");
}
}
}
public class ThreadTest{
public static void main(String[] args){
ThreadDemo tt = new ThreadDemo();//创建对象,在堆里
Thread t = new Thread(tt);//创建线程对象
t.start();
try{
Thread.currentThread().sleep(5000);
boolean b=t.isAlive();
System.out.println("t isAlive(): "+ b);
//t.stop();
t.interrupt();
//自然终止
// tt.stopRunning();
}
catch(InterruptedException e){
System.out.println(e);
}
}
}
输出结果:
falg = true
I love you
I love you
I love you
I love you
t isAlive(): true
I love you
运行过程梳理:
运行程序,主线程主方法执行, t.start(); 线程对象t进入就绪状态。主线程执行到 Thread.currentThread().sleep(5000); 主线程休息五秒,放弃处理机,线程t开始占领处理机,执行tt所指对象的run方法 ,输出flag=true , 然后执行Thread.currentThread().sleep(1000); 睡一秒,醒来后,输出 I love you ,此时主线程还在睡,所以t线程继续执行,继续循环,又睡一秒,醒来后,输出i love you,一直这样循环,当输出到第四个i love you,线程t第五次睡眠,此时主线程醒了,于是主线程继续执行,输出t isAlive(): true ,然后执行tt.stopRunning(); 调用tt所指对象的stopRunning方法,此时flag变为false。执行完后,主方法结束主线程死亡,接着线程t醒来继续执行(哪里睡着哪里醒来),输出i love you ,此时循环条件已经不满足,于是退出循环,线程结束。
这个方法的意思是,在主线程执行到这里,强行唤醒还在睡眠的t线程,使t线程进入就绪状态,给出中断线程的信号,t线程出现异常,捕获到中断异常,执行输出**“线程终止”**
最粗暴,主线程执行到这里,发现t线程还在存活,直接将睡眠的t线程终止,所以之后t线程没有任何输出。stop()可能会造成数据混乱,不建议使用
1、什么情况需要加锁?
一个线程执行某个代码块时不能中断,且只能由一个线程执行完成后才能下一个线程。比如购票系统、上厕所等原子性的操作,不能被其他线程中断,这时就要对原子性的操作进行加锁。但是加锁后并发性会降低。
2、
代码:购票例子
每一个对象都有一个监视器,或者叫做锁。保证原子操作不能被中断
/**
*演示多线程中的同步块和同步方法
*线程调度策略为时间片和轮转
*/
public class TicketsSystem1
{
public static void main(String[] args)
{
//创建对象
SellThread st=new SellThread();
new Thread(st).start();
new Thread(st).start();
new Thread(st).start();
}
}
class SellThread implements Runnable
{ int tickets=100;
Object obj=new Object();
public void run()
{ //int a=0;
while (this.tickets>0)
{
try
{
Thread.sleep(10);//第一个线程出来后锁会打开,此时让这个线程先休息一会,不然其他在锁池的线程可能没机会购买
}
catch(Exception e)
{
e.printStackTrace();
}
/*每一个对象都有一个监视器,或者叫做锁。保证原子操作不能被中断
把obj对象的锁加锁实现语句块同步*/
synchronized(obj) //判断这个对象是否开锁,线程执行到这里,如果锁是关闭的就进入锁池等待(线程为阻塞状态),如果锁是打开的,就执行线程,并关闭锁
{ //对语句块加锁,叫做块锁。要想执行下面的语句块(买票),必须当前锁是打开的,如果锁是关闭就进入锁池等待
try
{
Thread.sleep(10);//进入代码块的线程,这里即便睡眠,锁依然是关闭的,其他线程也进不来
}
catch(Exception e)
{
e.printStackTrace();
}
if(tickets>0)
{//this所指st对象的tickets
System.out.println("obj:"+Thread.currentThread().getName()+
" sell tickets:"+this.tickets);
this.tickets--;
System.out.println("obj:"+Thread.currentThread().getName()+
" sell tickets after:"+this.tickets);
}
}
}
}
}
**synchronized(obj) **
给OBJ对象加锁,以实现代码块同步。当线程执行到这里,如果锁是打开的,则执行下面的代码块,并且将锁关闭;其他线程来到这里,面对关闭的锁,只能进入锁池等待。
Thread.sleep(10);
执行代码块的线程,即便这里要睡眠,但此时锁依然是关闭的,只要线程还在执行,其他线程就不能进来打断它。这就是加锁的意义
Thread.sleep(10);
执行完买票代码后,锁就会开启。此时执行完买票操作的线程,还在运行,于是继续执行循环,到这里,系统让它睡眠,目的是为了给其他还在锁池等待的线程机会购票。否则刚买完票的线程可能又会被分配去买票。强制让买完票的线程放弃处理机。
3、锁的分类
4、方法锁实现同步
卖票代码:
package com.xqh.in;
/**方法锁实现同步*/
class ThreadDemo implements Runnable{
private int tickets = 20;
public void run(){
while(this.tickets != 0){//tickets==0,票卖完了,退出循环
try{
Thread.currentThread().sleep(10);//上一次买票的线程买完票后,锁打开,为防止它再次买票,强制让他睡眠,让出处理机,给其他在锁池的线程机会
}
catch(InterruptedException e){
System.out.println(e);
}
//调用this所指的ThreadDemo对象的sale方法
this.sale();
}
}
//私有方法只能在同一个类的其他方法里调用
private synchronized void sale(){//先看调用sale方法的对象(this所指对象)锁是否打开,如果是打开的,执行下面的代码,并把锁关锁;如果锁是关闭的,就进入锁池等待
if(this.tickets > 0){
System.out.println(Thread.currentThread().getName() + " : tickets = " + this.tickets);
try{
Thread.currentThread().sleep(100);
}
catch(InterruptedException e){
System.out.println(e);
}
this.tickets--;
System.out.println(Thread.currentThread().getName() + " : tickets after= " + this.tickets );
}
}
}
public class ThreadTest2{
public static void main(String[] args){
//创建实现了runnable接口的类的对象
ThreadDemo tt = new ThreadDemo();
//创建线程,并命名
Thread t1 = new Thread(tt, "t1");
Thread t2 = new Thread(tt, "t2");
Thread t3 = new Thread(tt, "t3");
Thread t4 = new Thread(tt, "t4");
//启动线程,线程进入就绪状态。得到时间片后就会占领处理机,执行tt所指对象的run方法
t1.start();
t2.start();
t3.start();
t4.start();
//执行到最后,主线程死亡
}
}
private synchronized void sale()
对方法加锁,而且是私有方法。线程执行到这里,先判断调用这个方法的对象(this所指对象)的锁是否打开,如果打开就继续执行下面的方法,锁是关闭的,则线程进入锁池等待。
5、消费者生产者问题
生产者线程生产物品,消费者线程消耗物品,但是生产者并不知道什么时候物品是空,消费者也不知道什么时候物品是有的。因此需要设计锁,使得生产者生产物品时,消费者不能进来打断,而当消费者消耗物品,生产者同样不能进来打断。生产者进来时如果已经有商品,就会执行wait()方法,使自己进入等待池,开启锁池,让消费者进来消费,消费者消费完,又会调用notify()方法,从等待池中释放生产者进入锁池去生产。
这个例子中,消费者生产者问题模拟的场景是取情报。一颗大树中间有个洞,生产者负责把情报放在洞里,消费者负责把情报取走,彼此并不知道什么对方什么时候放/取情报。示例代码如下:
package com.xqh.in;
public class Test1
{
public static void main(String[] args)
{
Tree q=new Tree();
Producer p=new Producer(q);
Consumer c=new Consumer(q);
p.start();
c.start();
}
}
class Producer extends Thread
{
Tree q;
Producer(Tree q)
{
this.q=q;
}
public void run()
{
for(int i=0;i<10;i++)
{
q.put(i);
System.out.println("Producer put "+i);
}
}
}
class Consumer extends Thread
{
Tree q;
Consumer(Tree q)
{
this.q=q;
}
public void run()
{
while(true)
{
System.out.println("Consumer get "+q.get());
}
}
}
//树类
class Tree
{
private int hole;//树洞
boolean bFull=false;
//放情报
public synchronized void put(int i)//给方法上锁,同时间只能有一个线程调用put/get方法。因为是对同一个树上锁
{
if(!this.bFull)//如果为空
{
this.hole=i;//把情报放入树洞
this.bFull=true;//设为非空
/*从该对象的等待队列中释放消费者线程进入该对象锁池
使该线程将再次成为可运行的线程*/
this.notify();
}
try
{//如果树洞里已经有情报了,让生产者线程进入this对象的等待队列,然后开启锁池让消费者来取
this.wait();
}
catch(Exception e)
{
e.printStackTrace();
}
}
//取情报
public synchronized int get()
{
if(!this.bFull)//如果为空
try
{
//开启锁池,自己进入等待队列。
this.wait();//如果树洞里没情报,让消费者线程进入this对象的等待队列,然后开启锁池,让生产者进来
}
catch(Exception e)
{
e.printStackTrace();
}
//锁池:当线程到加锁的方法,此时锁关闭,线程就会进入锁池
//等待池:一旦进入等待池,不能自动出来,要调用notify方法才能从等待池释放
bFull=false;;//设置为空
/*消费者取完情报,从该对象的等待队列中释放生产者线程进入该对象锁池
使该线程将再次成为可运行的线程*/
int value=this.hole;
this.notify();
return value;
}
//wait后自己进入等待池,必须等到对方notify,才能从等待池释放。
}
//陷入死循环。生产者放到9之后,生产者不再放情报,生产者线程死亡。但此时消费者并不知道,依旧
//执行线程,发现没有情报,于是执行wait让自己进入等待池,但此时再也没有生产者来唤醒它,于是
//陷入死循环。
输出结果:
Producer put 0
Consumer get 0
Consumer get 1
Producer put 1
Consumer get 2
Producer put 2
Consumer get 3
Producer put 3
Consumer get 4
Producer put 4
Consumer get 5
Producer put 5
Consumer get 6
Producer put 6
Consumer get 7
Producer put 7
Consumer get 8
Producer put 8
Consumer get 9
Producer put 9
创建两个线程对象,生产者线程对象p,消费者线程对象c;
生产者:
public synchronized void put(int i)
给方法上锁,这是放情报的方法。由生产者线程来调用,当生产者线程执行到这里,判断锁是否关闭,关闭则进入锁池等待,打开则执行调用方法;
this.notify();
如果树洞里为空,则放入情报(this.hole=i;),并将大树设为非空,然后调用this.notify(); ,意思是从该对象的等待队列中释放消费者线程进入锁池,使消费者线程再次成为可运行的线程。
this.wait();
如果生产者发现树洞里已经有情报了(this.bFull),则调用this.wait(),意思是让自己进入this对象的等待队列(等待消费者的notify唤醒),并开启锁池让消费者线程执行。
消费者:
public synchronized int get()
给消费者取情报的方法加锁。当消费者执行到这里,需要判断锁是否关闭,如果关闭则进入锁池等待,如果打开则执行get方法
this.wait()
如果树洞为空,那么消费者就没事可干,调用wait(),将自己线程进入等待队列,开启锁池,生产者线程占用处理机
this.notify()
如果树洞有情报,消费者将情报取走,将大叔设为空,然后调用this.notify(),意思是从该对象的等待队列中释放生产者线程进入锁池,使生产者线程再次成为可运行的线程
当生产者最后一次放入情报后,不再满足循环条件,线程结束,此时,消费者并不知道生产者已经不再投放情报,消费者执行到取情报代码,发现树洞为空,于是调用wait(),使其进入等待队列,但此时已经生产者来唤醒它了(notify()),于是程序会进入死循环。
每个对象不仅有锁池还有等待队列。进入等待队列的线程,无法自动出队列,只有等到线程notify()唤醒才能从等待队列释放进入锁池。而锁池里的线程,只要锁打开,就可以执行锁住的方法。
6、构造方法不能加锁,构造方法本身就属于线程安全的,不存在同步一说
1、ThreadLocal是什么
ThreadLocal叫做线程变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。每个Thread有自己的实例副本,且其他Thread不可访问,那就不存在多线程间共享的问题。提高并发性,之前多线程会对同一实例变量争用,导致数据混乱,用ThreadLocal则不会存在这个问题。
ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本 。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
2、ThreadLocal和synchronized区别
ThreadLocal和Synchonized都用于解决多线程并发访问,但有本质的区别:
3、简单使用
public class ThreadLocaDemo {
private static ThreadLocal<String> localVar = new ThreadLocal<String>();
static void print(String str) {
//打印当前线程中本地内存中本地变量的值
System.out.println(str + " :" + localVar.get());
//清除本地内存中的本地变量
localVar.remove();
}
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
public void run() {
ThreadLocaDemo.localVar.set("local_A");
print("A");
//打印本地变量
System.out.println("after remove : " + localVar.get());
}
},"A").start();
Thread.sleep(1000);
new Thread(new Runnable() {
public void run() {
ThreadLocaDemo.localVar.set("local_B");
print("B");
System.out.println("after remove : " + localVar.get());
}
},"B").start();
}
}
A :local_A
after remove : null
B :local_B
after remove : null
两个线程分表获取了自己线程存放的变量,他们之间变量的获取并不会错乱.
ThreadLocal set赋值的时候首先会获取当前线程thread,并获取thread线程中的ThreadLocalMap属性。如果map属性不为空,则直接更新value值,如果map为空,则先实例化threadLocalMap,并将value值初始化。
说明:当线程处于活动状态时它会持有该线程的局部变量的引用, 当该线程运行结束后,该线程拥有的局部变量都会结束生命周期。
ThreadLocal类主要由四个方法组成initialValue(),get(),set(T),remove()。在ThreadLocal类中有一个线程安全的Map,用于存储每一个线程的变量的副本。key是ThreadLocal,value是我们设置的value。
1、线程死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
public class DeadLockDemo {
private static Object resource1 = new Object();//资源 1
private static Object resource2 = new Object();//资源 2
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "线程 2").start();
}
}
假设线程1先执行,通过锁,得到资源1后,休息1s(此时并没有打开锁),然后线程2执行,通过锁,得到资源2,休息1s(没有打开锁),然后线程1醒来,想要获取资源2,但是获取不到(因为锁是关闭的),而线程2醒来想要获取资源1,彼此都请求获取对方的资源,两个线程陷入互相等待的状态,产生死锁。
2、产生死锁的四个必要条件:
3、如何预防和避免死锁
破坏死锁产生的必要条件即可
在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。
1、悲观锁
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题,所以每次在获取资源操作的时候都会上锁。共享资源每次只给一个线程使用,其他线程阻塞,用完后再把资源转让给其他线程
Java中的synchronized和ReentrantLock等独占锁就是悲观锁思想的实现
高并发的场景下,激烈的锁竞争会造成线程阻塞,大量线程阻塞会导致系统的上下文切换(更换线程时,保存当前线程的上下文以及加载下一个线程的上下文),增加系统的性能开销。并且,悲观锁还可能会存在死锁问题,影响代码的正常运行。
2、乐观锁
乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改时去验证相对应的资源是否被其他线程所修改 。(一般用版本号机制/CAS算法实现)
高并发的场景下,乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁问题,在性能上更胜一筹。但是如果写占比很多的情况,会频繁失败和重试,同样会影响性能。
3、使用场合
理论上来说:
4、实现乐观锁
一般是在数据表中加上一个数据版本号version,表示数据被修改的次数,当数据被修改时,version值会加1。例如,当线程A要更新数值时,在读取数据的同时也会读取version值,提交更新时,若刚才读取到的version值和当前数据库中的version值相等才更新(防止在读取期间其他线程对数据进行修改导致版本号不一致),否则重试更新操作。
CAS思想很简单,就是用一个预期值(这个变量的值)和要更新的变量值去进行比较,两值相等才会进行更新。
可能存在问题:
ABA问题,如果一个变量V初次读取是A值,并且准备赋值时它仍然是A值,能说明它的值就一定没有被修改吗?是不能的,因为在这段时间,可能其他线程对它修改值为B,然后又改回A,那么CAS就会认为它从来没有被修改过。—解决方案加上版本号
1、ReentrantLock是什么
ReentrantLock实现了Lock接口,是一个可重入且独占式的锁,和synchronized关键字类似。不过,ReentrantLock更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。
2、公平锁和非公平锁区别
1、在java中,volatile关键字可以保证变量的可见性。如果我们将变量声明为volatile,这就指示JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
2、在java中,volatile关键字除了可以保证变量的可见性,还有一个重要的作用就是防止JVM的指令重排序。
3、volatile关键字能保证变量的可见性,但不能保证对变量的操作是原子性的。
4、volatile和synchronized的区别