文章目录
为了弄懂什么是线程安全,首先必须要了解的两个概念,进程和线程。
一.什么是进程
通俗而言,进程就是电脑程序管理器中可以看到,不同的程序同时允许,比如边用QQ聊天,边听着QQ音乐。QQ和QQ音乐就是两个不同的进程。
二.什么是线程
进程想要执行任务必须依赖于线程,也就是说,线程是进程的最小单位,并且一个进程中至少有一个线程。比如在使用QQ音乐时,听歌和缓存歌曲就是QQ音乐的两个线程。
三.什么是多线程
多线程,顾名思义就是多个线程。比如在使用QQ音乐时,边听歌,边缓存歌曲,边写评论就是多线程。多线程又分为并行和串行。
- 串行是指的,比如我们用迅雷同时下载3个文件,在串行的概念下,只有在文件A完成下载以后,B文件才会下载,C文件要等B文件下载完再开始下载。
- 并行是指的,比如我们用迅雷同时下载3个文件,在并行的概念下,开启多条线程,3个文件可以同时下载。
四.线程安全的由来
了解了关于线程和进程的相关知识,单线程永远都是做完一件事情再做另外的一件事情,因此单线程永远不会出现差错,但是相反,多线程有多个线程同时进行,因此会有错误产生,因此线程安全(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锁
- Lock锁是显式锁,需要手动关闭手动打开,synchronized锁是隐式锁。
- Lock锁种类很多,性能优越。
- Lock锁JVM调用效率更高。