教你弄懂线程安全(史上最详)


为了弄懂什么是线程安全,首先必须要了解的两个概念,进程和线程。

一.什么是进程

通俗而言,进程就是电脑程序管理器中可以看到,不同的程序同时允许,比如边用QQ聊天,边听着QQ音乐。QQ和QQ音乐就是两个不同的进程。

二.什么是线程

进程想要执行任务必须依赖于线程,也就是说,线程是进程的最小单位,并且一个进程中至少有一个线程。比如在使用QQ音乐时,听歌和缓存歌曲就是QQ音乐的两个线程。

三.什么是多线程

多线程,顾名思义就是多个线程。比如在使用QQ音乐时,边听歌,边缓存歌曲,边写评论就是多线程。多线程又分为并行和串行。

  • 串行是指的,比如我们用迅雷同时下载3个文件,在串行的概念下,只有在文件A完成下载以后,B文件才会下载,C文件要等B文件下载完再开始下载。
文件A
文件B
文件C
  • 并行是指的,比如我们用迅雷同时下载3个文件,在并行的概念下,开启多条线程,3个文件可以同时下载。
同一时刻发生
文件A
文件B
文件C

四.线程安全的由来

了解了关于线程和进程的相关知识,单线程永远都是做完一件事情再做另外的一件事情,因此单线程永远不会出现差错,但是相反,多线程有多个线程同时进行,因此会有错误产生,因此线程安全(thread safe)由此而生。

五.什么是线程安全

当一个类已经很好地同步以保护他的数据时,这个类就称为线程安全。相反的,线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成某些线程得到的是无效数据。
所以,线程安全问题都是由多个线程对共享的变量进行读写引起的。

六.举例

大家考虑以下下面的代码:

private int myInt = 0;
public int AddOne()
{
    int tmp = myInt;
    tmp = tmp + 1;
    myInt = tmp;
    return tmp;
}

现在线程A和线程B都想执行AddOne()。但是A首先启动并将myInt(0)的值读入tmp。现在由于某种原因,CPU决定停止线程A并将执行延迟到线程B。线程B现在也将myInt的值(仍然是0)读入它自己的变量tmp。线程B完成了整个方法,所以最后myInt = 1。返回1。现在又轮到A线了。一个线程继续执行。tmp+1(线程A的tmp为0),然后将该值保存在myInt中。myInt还是1。
在这里插入图片描述
AddOne()方法调用了两次,最后返回值应该是2,却为1。因此这里存在线程不安全问题。


再举一个具体的实例:
现在有6张火车票需要出售,现有三个售货员A,B,C,分别售票。

package com.Thread.test;

public class TestThread implements Runnable
{
	private int tickets = 6;

	@Override
	public void run()
	{
		for(int i=0;i<50;i++) {
			if(tickets>0) {
				//在此加入线程休眠为了让其他的线程来争抢cpu资源,会出现不安全线程现象
				//睡眠的目的是线程切换,更易发生争抢
				try
				{
					Thread.sleep(100);
				}
				catch (InterruptedException e)
				{
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName()+"卖票,ticket= "+ tickets--);
			}
		}
		
	}
}
package com.Thread.test;

public class Test1
{

	public static void main(String[] args) {
		TestThread t1 = new TestThread(); 
		new Thread(t1,"售票员A").start();
		new Thread(t1,"售票员B").start();
		new Thread(t1,"售票员C").start();
	}
}

Output:

售票员C卖票,ticket= 5
售票员B卖票,ticket= 6
售票员A卖票,ticket= 6
售票员C卖票,ticket= 4
售票员B卖票,ticket= 3
售票员A卖票,ticket= 2
售票员B卖票,ticket= 0
售票员C卖票,ticket= 1
售票员A卖票,ticket= -1

出现了重复票数,0张票数,甚至票数为负的现象。足以证明,线性不安全!!!

七. 生活中的案例

这就好比,有三个人想上厕所,但是厕所只有一个。三个人同时去抢一个厕所,一个人还没上完厕所,另一个人就冲进去了,所以会导致混乱。在现实生活中,我们可以在厕所上加一把锁,第一个人去上厕所的时候锁上门,就不会有其他人的进入,只有等到这个(this)人用完厕所之后,下一位才能上厕所。这里所提到的厕所就是CPU分配的资源,三个人就是三个线程,锁就是接下来要讲解的synchronized对象锁。

八.如何解决线程不安全问题

1⃣️ synchronized对象锁

synchronized关键字,就是用来控制线程同步的,保证我们的线程在多线程环境下,不被多个线程同时执行,确保我们数据的完整性。

1.1同步代码块

同步代码块是使用synchronized关键字定义的代码块,但是在同步的时候需要设置对象锁,一般会给当前对象this上锁。

package com.Thread.test;

public class TestThread implements Runnable
{
	private int tickets = 6;

	@Override
	public void run()
	{
		for(int i=0;i<50;i++) {
			synchronized(this) {  //this表示谁调用run()方法,this就是谁,即当前对象!
			if(tickets>0) {
				
				try
				{
					Thread.sleep(100);
				}
				catch (InterruptedException e)
				{
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName()+"卖票,ticket= "+ tickets--);
			}
			}
		}
		
	}
}

package com.Thread.test;

public class Test1
{
	public static void main(String[] args) {
		TestThread t1 = new TestThread(); 
		new Thread(t1,"售票员A").start();
		new Thread(t1,"售票员B").start();
		new Thread(t1,"售票员C").start();
	}
}

Output:

售票员A卖票,ticket= 6
售票员C卖票,ticket= 5
售票员C卖票,ticket= 4
售票员C卖票,ticket= 3
售票员C卖票,ticket= 2
售票员C卖票,ticket= 1

1.2同步方法

如果一个方法上使用了synchronized定义,那么该方法称为同步方法。

就是将需要同步的代码块(synchronized所包围的代码块)抽取出来写出单独的同步方法即可

package com.Thread.test;

public class TestThread implements Runnable
{
	private int tickets = 6;

	@Override
	public void run()
	{
		for(int i=0;i<50;i++) {
			
			this.sale(); //调用同步方法
			
		}
	}

	public synchronized  void sale() {  //同步方法
		if(tickets>0) {
			
			try
			{
				Thread.sleep(100);
			}
			catch (InterruptedException e)
			{
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+"卖票,ticket= "+ tickets--);
		}
	}
}

package com.Thread.test;

public class Test1
{

	public static void main(String[] args) {
		TestThread t1 = new TestThread(); 
		new Thread(t1,"售票员A").start();
		new Thread(t1,"售票员B").start();
		new Thread(t1,"售票员C").start();
		
	}
}

Output:

售票员A卖票,ticket= 6
售票员C卖票,ticket= 5
售票员C卖票,ticket= 4
售票员C卖票,ticket= 3
售票员C卖票,ticket= 2
售票员C卖票,ticket= 1

1.3问题

上述代码,实现了Runnable接口,我们在测试类中创建了一个线程对象t1,然后将t1取名为不同的售货员

        TestThread t1 = new TestThread(); 
		new Thread(t1,"售票员A").start();
		new Thread(t1,"售票员B").start();
		new Thread(t1,"售票员C").start();

我们用synchronized解决了线程不安全问题,但是,这把锁,锁住的是this对象,即同步代码块synchronized(this){...}和同步方法this.sale()。this表示的是调用方法的对象,在上述代码中都是t1在调用run()方法,因此this就是t1相当于有一个线程对象,一把锁。

如果我们继承Thread接口,在测试类创建三个线程对象,分别取名为不同的售货员呢??

package com.Thread.test;

public class TestThread extends Thread
{
	private static int tickets = 6; //用static原因:让三个售票员共享这6张票
	
	//构造器:设置线程名称
	public TestThread(String name)
	{
		super(name);
		
	}

	@Override
	public void run()
	{
		for(int i=0;i<50;i++) {
			
			synchronized(this) {
				if(tickets>0) {
			
			try
			{
				Thread.sleep(100);
			}
			catch (InterruptedException e)
			{
				e.printStackTrace();
			}
			System.out.println(this.getName()+"卖票,ticket= "+ tickets--);
			}
			}
		}
			
	}
}

package com.Thread.test;

public class Test1
{

	public static void main(String[] args) {
		TestThread t1 = new TestThread("售货员A"); 
		t1.start();
		TestThread t2 = new TestThread("售货员B"); 
		t2.start();
		TestThread t3 = new TestThread("售货员C"); 
		t3.start();
	}	
}

Output:

售货员B卖票,ticket= 6
售货员C卖票,ticket= 5
售货员A卖票,ticket= 5
售货员C卖票,ticket= 4
售货员B卖票,ticket= 3
售货员A卖票,ticket= 2
售货员C卖票,ticket= 1
售货员B卖票,ticket= 0
售货员A卖票,ticket= -1

再次出现了线程不安全现象,为什么会这样??

原因:此段代码中,测试类中创建三个线程对象t1,t2,t3,分别取名为不同的售货员,并且还是用synchronized(this)的方法来同步线程。此时的this表示谁调用run()方法谁就是this,那被锁住的this可能是t1,t2,t3中的任意一个,相当于有三个线程对象,三把锁。 每个对象,只看自己的那把锁,我们要想线程安全,应该是唯一的一把锁,来锁住所有的线程对象。

1.4如何解决??

我们只需要将这把公用的锁,变成唯一的锁即可!
如果synchronized(“aaa”),用“aaa“来代替this,就不会让每个调用run()方法的对象有分别自己的锁,即实现了唯一锁

  • 我们通常使用synchronized(当前类名.class)的形式,实现唯一锁
  • synchronized(“aaa”),括号中的必须是引用数据类型
package com.Thread.test;

public class TestThread extends Thread
{
	private static int tickets = 6; //用static原因:让三个售票员共享这6张票
	
	//构造器:设置线程名称
	public TestThread(String name)
	{
		super(name);
		
	}

	@Override
	public void run()
	{
		for(int i=0;i<50;i++) {
			
			synchronized(TestThread.class) {  //当前类名.class
				if(tickets>0) {
			
			try
			{
				Thread.sleep(100);
			}
			catch (InterruptedException e)
			{
				e.printStackTrace();
			}
			System.out.println(this.getName()+"卖票,ticket= "+ tickets--);
			}
			}
		}
			
	}
}

package com.Thread.test;

public class Test1
{

	public static void main(String[] args) {
		TestThread t1 = new TestThread("售货员A"); 
		t1.start();
		TestThread t2 = new TestThread("售货员B"); 
		t2.start();
		TestThread t3 = new TestThread("售货员C"); 
		t3.start();
		
		
	}	
}

Output:

售货员A卖票,ticket= 6
售货员A卖票,ticket= 5
售货员A卖票,ticket= 4
售货员A卖票,ticket= 3
售货员A卖票,ticket= 2
售货员C卖票,ticket= 1

完美的解决了问题!

1.5线程同步的缺点

  • 效率低,安全性高
  • 容易出现死锁现象

1.6死锁现象

什么是死锁现象呢?

  • 举一个现实生活中的例子:两个人A,B在吃饭,A拿到一根筷子和一根刀子,B拿到一把叉子和一根筷子,A对B说你先给我筷子我再给你勺子,B对A说你先给我刀子我再给你筷子。这就形成了死循环,两个人永远无法吃饭。
  • 最常见的死锁现象就是,线程1持有对象A的锁,而且正在等待对象B上的锁;而线程2持有对象B上的锁,却正在等待对象A上的锁。俩线程永远不会获得第二个锁或释放第一个锁,所以它们永远等待下去。
package com.Thread.test;

public class DeadLock implements Runnable
{
	public int flag=1;
	static Object o1 = new Object();
	static Object o2 = new Object();

	@Override
	public void run()
	{
		System.out.println("flag:" + flag);
		//当flag==1,锁住o1
		if(flag==1) {
			synchronized(o1) {
				try
				{
					Thread.sleep(100);
				}
				catch (InterruptedException e)
				{
					e.printStackTrace();
				}
				//想把o2锁住然后打印信息
				synchronized(o2) {
					System.out.println("when flag==1, o1 and o2 has been locked");
				}
			}
		}
		//当flag==2,锁住o2
		if(flag==0) {
			synchronized(o2) {
				try
				{
					Thread.sleep(100);
				}
				catch (InterruptedException e)
				{
					e.printStackTrace();
				}
				//想把o1锁住然后打印信息
				synchronized(o1) {
					System.out.println("when flag==2, o1 and o2 has been locked");
				}	
		}	
	}
}
}

package com.Thread.test;

class Test{
	
	public static void main(String[] args)
	{
		DeadLock dl1 = new DeadLock();
		DeadLock dl2 = new DeadLock();
		dl1.flag=1;
		dl2.flag=0;
		new Thread(dl1).start();
		new Thread(dl2).start();
		
	}
}	

Output:

flag:1
flag:0

两个对象dl1,dl2都进入到run()方法后,分别对应执行if(flag ==0){…} 和 if(flag ==1){…}

  • 当flag==1,锁住o1,然后想锁住o2再打印信息,但是o2已经在第二个if中锁住了,所以无法往下执行无法打印信息。
  • 同理,当flag= =0时,锁住o2,然后想锁住o1再打印信息,但是o1已经在第一个if中锁住了,所以无法往下执行无法打印信息。
  • 所以控制台中只有打印出flag:1 flag:0

如何解决死锁问题???

package com.Thread.test;

public class DeadLock implements Runnable
{
	public int flag=1;
	static Object o1 = new Object();
	static Object o2 = new Object();

	@Override
	public void run()
	{
		System.out.println("flag:" + flag);
		//当flag==1,锁住o1
		if(flag==1) {
			synchronized(o1) {
				try
				{
					Thread.sleep(100);
				}
				catch (InterruptedException e)
				{
					e.printStackTrace();
				}
			}
			//想把o2锁住然后打印信息
			synchronized(o2) {
				System.out.println("when flag==1, o1 and o2 has been locked");
			}
		}
		//当flag==2,锁住o2
		if(flag==0) {
			synchronized(o2) {
				try
				{
					Thread.sleep(100);
				}
				catch (InterruptedException e)
				{
					e.printStackTrace();
				}
			
		}
			//想把o1锁住然后打印信息
			synchronized(o1) {
				System.out.println("when flag==2, o1 and o2 has been locked");
			}
		
	}
	}
}

Output:

flag:1
flag:0
when flag==1, o1 and o2 has been locked
when flag==2, o1 and o2 has been locked

把放在o1锁中的o2锁拿到o1锁循环的外面,把放在o2锁中的o1锁拿到o2锁循环外面。
不要锁套锁!!!

2⃣️Lock锁

首先我们看一下Lock源码:

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

很明显Lock是一个接口,里面是未实现的方法。
说到接口,我们在创建Lock对象时就不能Lock ic = new Lock();,因为接口不能创建对象。但是可以通过实现类创建对象,比如Lock ic = new ReentrantLock();

我们可以用lock()和unlock()来上锁解锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。

Lock lock = ...;
lock.lock();
try{
    //处理任务
}catch(Exception ex){
     
}finally{
    lock.unlock();   //释放锁
}

2.1Lock锁与synchronized锁

  1. Lock锁是显式锁,需要手动关闭手动打开,synchronized锁是隐式锁。
  2. Lock锁种类很多,性能优越。
  3. Lock锁JVM调用效率更高。
  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值