线程安全问题概述
当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条记录等)的时候,若多个线程只有读操作,那么不会发生线程安全问题,但是如果多个线程中对资源有读和写的操作,就容易出现线程安全问题。 下面我们来总结一下,什么情况下会产生共享资源。
什么是情况下是共享资源
- 局部变量不能共享,局部变量是每次调用方法都是独立的
- 不同的实例对象的实例变量是独立的。
- 一个类的静态变量是共享的
- 同一个对象的实例变量共享
经典案例,演示线程的安全问题。电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “小蝌蚪找妈妈”,本次电影的座位共100个(本场电影只能卖100张票)。我们来模拟电影院的售票窗口,实现3个窗口同时卖。
定义类,模拟票
public class MyThread implements Runnable {
//共享资源
private int ticket = 100;
@Override
public void run() {
while (true) {
//票的数量小于1就停止卖票
if (ticket < 1) {
break;
}
//睡眠100毫秒,模拟收钱时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在卖票" + ticket+"张票");
//每次卖票就减少票的数量
ticket--;
}
}
}
定义测试类
public class Test {
public static void main(String[] args) {
//线程任务对象
MyThread mt = new MyThread();
//创建3个线程对象,模拟三个窗口
Thread t1 = new Thread(mt, "窗口1");
Thread t2 = new Thread(mt, "窗口2");
Thread t3 = new Thread(mt, "窗口3");
//启动线程,模拟开始卖票
t1.start();
t2.start();
t3.start();
}
}
发现程序出现了两个问题:
- 相同的票数,比如某张票被卖了两回。
- 不存在的票,比如0票与-1票,是不存在的。
这种问题,几个窗口(线程)票数不同步了,这种问题称为线程不安全。
多线程造成线程安全问题的原因:
- 线程的调度是抢占式的,任何线程在任何时刻都有可能失去对CPU的使用权
- 线程任务结束之后,该线程就会被销毁
- 主线程必须等子线程执行完毕之后,才会结束
解决线程安全问题
要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制 (synchronized)来解决。
根据案例简述:
窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。
为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。
synchronized关键字
synchronized 表示“同步”的。它可以对“多行代码”进行“同步”——将多行代码当成是一个完整的整体,一个线程如果进入到这个代码块中,会全部执行完毕,执行结束后,其它线程才会执行。这样可以保证这多行的代码作为完整的整体,被一个线程完整的执行完毕。synchronized被称为“重量级的锁”方式,也是“悲观锁”——效率比较低。
锁的范围问题
- 锁的范围太小:不能解决安全问题
- 锁的范围太大:因为一旦某个线程抢到锁,其他线程就只能等待,所以范围太大,效率会降低,不能合理利用CPU资源。
同步代码块
synchronized关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
格式:
注意事项
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.
- 锁对象 可以是任意类型。
- 多个线程对象 要使用同一把锁。、
在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。下面我们使用同步代码块来解决线程安全问题
public class MyThread implements Runnable {
//共享资源
private int ticket = 100;
@Override
public void run() {
while (true) {
//锁对象,可以是任意类型,但必须是唯一的
synchronized (this) {
//票的数量小于1就停止卖票
if (ticket < 1) {
break;
}
//睡眠100毫秒,模拟收钱时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在卖票" + ticket + "张票");
//每次卖票就减少票的数量
ticket--;
}
}
}
}
同步方法
使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
格式:
同步锁是谁?
- 对于非static方法,同步锁就是this。
- 对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。
下面我们使用同步方法来解决线程安全问题
public class MyThread implements Runnable {
//共享资源
private int ticket = 100;
@Override
public void run() {
while (true) {
if (sellTickets()) break;
}
}
//同步方法
private synchronized boolean sellTickets() {
//锁对象,可以是任意类型,但必须是唯一的
//票的数量小于1就停止卖票
if (ticket < 1) {
return true;
}
//睡眠100毫秒,模拟收钱时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在卖票" + ticket + "张票");
//每次卖票就减少票的数量
ticket--;
return false;
}
}
开发中,一条线程使用的是同步代码块,一条线程使用的是同步方法,但这2条线程需要实现同步,实现这个需求。同步代码块和同步方法的锁对象必须一致,而同步方法的锁对象是默认的,所以必须清楚同步方法的锁对象
自定义任务类
public class Demo {
public void method1() {
System.out.println("张三开门");
System.out.println("张三脱衣服");
try {
//洗澡时间
Thread.sleep(1000);
System.out.println("张三洗澡");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("张三洗完了");
System.out.println("张三出去");
}
//同步方法
public synchronized void method2() {
System.out.println("李四开门");
System.out.println("李四脱衣服");
try {
//洗澡时间
Thread.sleep(1000);
System.out.println("李四洗澡");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("李四洗完了");
System.out.println("李四出去");
}
}
自定义测试类
// 我们必须保证不同的线程对象,锁对象是一样的才能保证线程安全
public class Test {
public static void main(String[] args) {
Demo d = new Demo();
//线程对象 1
new Thread(new Runnable() {
@Override
public void run() {
//同步代码块
synchronized (d) {
d.method1();
}
}
}).start();
//线程对象 2
new Thread(new Runnable() {
@Override
public void run() {
//使用的同步方法
d.method2();
}
}).start();
}
}
Lock锁
java.util.concurrent.locks.Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更加面向对象Lock锁也称同步锁,加锁与释放锁方法化了,如下:
- public void lock() :加同步锁。
- public void unlock():释放同步锁
如果锁对象没有被释放,则线程永远不会结束。子线程,不会结束,主线程也永远不会结束。下面我们使用lock锁来解决线程安全问题
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyThread implements Runnable {
//共享资源
private int ticket = 100;
//创建唯一对象
Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
//加锁
lock.lock();
//票的数量小于1就停止卖票
if (ticket < 1) {
lock.unlock();
break;
}
//睡眠100毫秒,模拟收钱时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在卖票" + ticket+"张票");
//每次卖票就减少票的数量
ticket--;
//释放锁
lock.unlock();
}
}
}
高并发及线程安全
高并发:
线程安全:
- 在某个时间点上,当大量用户(线程)访问同一资源时,由于多线程运行机制的原因,可能会导致被访问的资源出现"数据污染"的问题。"数据污染"就是线程安全问题。
多线程的运行机制
- 当一个线程启动后,JVM会为其分配一个独立的"线程栈区",这个线程会在这个独立的栈区中运行。
- 多个线程在各自栈区中独立、无序的运行。抢到CPU执行权就运行,失去就停止。
查看下面代码,并且分析出代码在内存中执行的过程
//线程类
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("小强: " + i);
}
}
}
public class Test {
public static void main(String[] args) {
//启动子线程
new MyThread().start();
//主线程任务
for (int i = 0; i < 20; i++) {
System.out.println("旺财: " + i);
}
}
}
流程图: ?
程序启动运行main时候,java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着调用mt的对象的 start方法,另外一个新的线程也启动了,这样,整个应用就在多线程下运行。
通过这张图我们可以很清晰的看到多线程的执行流程,那么为什么可以完成并发执行呢?我们再来讲一讲原理。 多线程执行时,到底在内存中是如何运行的呢?以上个程序为例,进行图解说明:
多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。
当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那么进程就结束了
多线程的安全性问题产生的本质原因
可见性
概述:一个线程没有看见另一个线程对共享变量的修改。
原因:Java的内存模型(Java Memory Model)决定的。描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。 简而言之: 就量值是所有共享变量都是存在主内存中的,线程在执行的时候,有单独的工作栈内存,会把共享变量拷贝一份到线程的单独工作内存中,并且对变量所有的操作,都是在单独的工作内存中完成的,不会直接读写主内存中的变
例如下面的程序,先启动一个线程,在线程中将一个变量的值更改,而主线程却一直无法获得此变量的新值。
自定义线程类
public class MyThread extends Thread {
// 共享变量(主和子线程共享)
static boolean flag = false;
@Override
public void run() {
// 暂停5秒
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 修改flag的值
flag = true;
System.out.println("子线程把flag的值修改为true了");
}
}
自定义测试类
public class Test {
public static void main(String[] args) {
/*
多线程的安全性问题-可见性:
概述: 一个线程没有看见另一个线程对共享变量的修改
例如:先启动一个线程,在线程中将一个变量的值更改,而主线程却一直无法获得此变量的新值。
*/
// 创建并启动线程
MyThread mt = new MyThread();
mt.start();
// 主线程
while (true){
if (MyThread.flag == true){
System.out.println("结束死循环");
break;
}
}
/*
期望结果: 子线程修改共享变量flag的值为true后,主线程就会结束死循环
实际结果: 子线程修改共享变量flag的值为true后,主线程没有结束死循环
原因: 子线程对共享变量flag修改后的值,主线程不可见
由于死循环是非常简单,接近底层的代码,所以只需速度非常的快,来不及去主
内存中从新获取新的值,所以主线程工作内存中的flag的值一直是false,一直死循环
如果某一个时刻,主线程去主内存中从新获取修改后的flag值,就会结束死循环,
但主线程什么时候会去主内存中获取修改后的flag值,我们不确定,所以可能存在多线程
可见性问题
*/
}
}
?有序性
有些时候“编译器”在编译代码时,会对代码进行“重排”。
- 单线程情况下:第一行和第二行可能会被“重排”:可能先编译第二行,再编译第一行,总之在执行第三行之前,会将1,2编译完毕。1和2先编译谁,不影响第三行的结果。
- 多线程情况下:代码重排,可能会对另一个线程访问的结果产生影响。多线程环境下,我们通常不希望对一些代码进行重排的!!
原子性
所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行,多个操作是一个不可以分割的整体。
演示如果没有原子性,造成的线程安全问题
class MyThread extends Thread {
// 共享变量
static int i = 0;
@Override
// 任务:对共享变量i自增100万次
public void run() {
for (int j = 0; j < 1000000; j++) {
i ++;
}
System.out.println("子线程执行完毕了");
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
// 创建线程对象, 启动线程
new MyThread().start();
// 主线程对共享变量i自增100万次
for (int i = 0; i < 1000000; i++) {
MyThread.i ++;
}
// 暂停,保证主线程和子线程都对共享变量a自增完了100万次,再统计i的结果
Thread.sleep(5000);
// 打印最终共享变量i的值
System.out.println("最后结果 " + MyThread.i);
/*
期望:最终a的值为200000
最后结果 1722621
*/
}
}
实际结果和期望结果不同的原因:两个线程对共享变量的操作产生覆盖的效果
volatile关键字
概述
- volatile是一个"变量修饰符",它只能修饰"成员变量",它能强制线程每次从主内存获取值,并能保证此变量不会被编译器优化。
- volatile能解决变量的可见性、有序性丶volatile不能解决变量的原子性
volatile解决可见性,有序性问题示例
class MyThread implements Runnable {
volatile boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("修改了,成为true");
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
//子线程
MyThread mt = new MyThread();
new Thread(mt).start();
// 主线程
while (true){
if (mt.flag == true){
System.out.println("结束死循环");
break;
}
}
}
}
原子类
在java.util.concurrent.atomic包下定义了一些对“变量”操作的“原子类”
- AtomicInteger:对int变量操作的“原子类”;
- AtomicLong:对long变量操作的“原子类”;
- AtomicBoolean:对boolean变量操作的“原子类”;
- .....
它们可以保证对“变量”操作的:原子性、有序性、可见性。
AtomicInteger类示例
import java.util.concurrent.atomic.AtomicInteger;
class MyThread extends Thread {
// 共享变量
static AtomicInteger i = new AtomicInteger( 0); // 参数表示变量的初始化值
@Override
// 任务:对共享变量i自增100万次
public void run() {
for (int j = 0; j < 1000000; j++) {
//使用方法代替++
i.getAndIncrement();// 相当于i++
}
System.out.println("子线程执行完毕了");
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
// 创建线程对象, 启动线程
new MyThread().start();
// 主线程对共享变量i自增100万次
for (int i = 0; i < 1000000; i++) {
MyThread.i.getAndIncrement();// 相当于i++
}
// 暂停,保证主线程和子线程都对共享变量a自增完了100万次,再统计i的结果
Thread.sleep(5000);
// 打印最终共享变量i的值
System.out.println("最后结果 " + MyThread.i);
/*
期望:最终a的值为20000000
最后结果 20000000
*/
}
}
AtomicInteger类的工作原理-CAS机制
数组的多线程并发访问的安全性问题 ?
假如创建int类型的数组并且默认初始化,创建1000个线程,每个线程为数组的每个元素+1。正常情况,数组的每个元素最终结果应为:1000。可以发现,有些元素并不是1000.。为保证数组的多线程安全,Java中提供了数组操作的原子类:
- AtomicIntegetArray:对int数组操作的原子类
- AtomicLongArray:对long数组操作的原子类
- AtomicReferenceArray:对引用类型数组操作的原子类
- ....
代码演示
import java.util.concurrent.atomic.AtomicIntegerArray;
class MyThread extends Thread {
// public static int[] arr = new int[100000];
//改用原子类,使用数组构造
public static AtomicIntegerArray arr = new AtomicIntegerArray(100000);
@Override
public void run() {
for (int i = 0; i < arr.length(); i++) {
arr.addAndGet(i, 1);//将i位置上的元素 + 1
}
}
}
class Test{
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 4000; i++) {
new MyThread().start();//创建4000个线程,每个线程为数组的每个元素+1
}
Thread.sleep(1000 * 5);//让所有线程执行完毕
System.out.println("主线程休息5秒醒来");
for (int i = 0; i < MyThread.arr.length(); i++) {
System.out.println(MyThread.arr.get(i));
}
}
}
并发包
在JDK的并发包里提供了几个非常有用的并发容器和并发工具类。供我们在多线程开发中进行使用。
- ArrayList线程不安全,我们可以使用CopyOnWriteArrayList来代替ArrayList做到线程安全
- HashSet线程不安全,我们可以使用CopyOnWriteArraySet来代替HashSet做到线程安全
- HashMap线程不安全,我们可以使用Hashtable或者ConcurrentHashMap 来代替HashMap做到线程安全。
Hashtable和ConcurrentHashMap的区别
?结论: 建议使用ConcurrentHashMap
并发工具类
CountDownLatch类:允许一个或多个线程等待其他线程完成操作。
构造方法:
- public CountDownLatch(int count):初始化一个指定计数器的CountDownLatch对象。当计数器的值为0,那么等待结束。
重要方法:
- public void await() throws InterruptedException:让当前线程等待
- public void countDown(): 计数器进行减1
线程1要执行打印:A和C,线程2要执行打印:B,但线程1在打印A后,要线程2打印B之后才能打印C,所以:线程1在打印A后,必须等待线程2打印完B之后才能继续执行。
import java.util.concurrent.CountDownLatch;
class T1 extends Thread {
CountDownLatch cdl;
public T1(CountDownLatch cdl) {
this.cdl = cdl;
}
@Override
public void run() {
//打印A
System.out.println("打印A...");
//调用await()方法进入等待(线程2打印B)
try {
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印C
System.out.println("打印C...");
}
}
class T2 extends Thread {
CountDownLatch cdl;
public T2(CountDownLatch cdl) {
this.cdl = cdl;
}
@Override
public void run() {
// 打印B
System.out.println("打印B...");
// 调用countDown()方法让计数器-1
cdl.countDown();
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
/*
注意:
1.创建的CountDownLatch对象的计数器初始值为1
2.线程1和线程2使用的CountDownLatch对象要一致
*/
// 创建CountDownLatch对象,指定计数器的值为1
CountDownLatch cdl = new CountDownLatch(1);
// 创建并启动线程
new T1(cdl).start();
Thread.sleep(5000);
new T2(cdl).start();
}
}
CyclicBarrier类
CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。
使用场景:CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。
构造方法
public CyclicBarrier(int parties, Runnable barrierAction)
//parties: 代表要达到屏障的线程数量
//barrierAction:表示达到屏障后要执行的线程
重要方法:
public int await()// 每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞
例如:公司召集5名员工开会,等5名员工都到了,会议开始。我们创建5个员工线程,1个开会线程,几乎同时启动,使用CyclicBarrier保证5名员工线程全部执行后,再执行开会线程。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
class MyRunnable implements Runnable {
CyclicBarrier cb;
public MyRunnable(CyclicBarrier cb) {
this.cb = cb;
}
@Override
public void run() {
// 到达会议室
System.out.println(Thread.currentThread().getName()+":到达了会议室");
//调用await()方法告诉CyclicBarrier,当前线程到了屏障,然后当前线程阻塞
try {
cb.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
//离开会议室
System.out.println(Thread.currentThread().getName()+":离开会议室");
}
}
public class Test {
public static void main(String[] args) {
CyclicBarrier cb = new CyclicBarrier(5, new Runnable() {
@Override
public void run() {
System.out.println("开始开会");
try {
//模拟开会时间
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("下班了...");
}
});
// 创建任务
MyRunnable mr = new MyRunnable(cb);
// 创建5条线程
new Thread(mr,"员工1").start();
new Thread(mr,"员工2").start();
new Thread(mr,"员工3").start();
new Thread(mr,"员工4").start();
new Thread(mr,"员工5").start();
}
}
Semaphore类
- Semaphore的主要作用是控制线程的并发数量。
- synchronized可以起到"锁"的作用,但某个时间段内,只能有一个线程允许执行。
- Semaphore可以设置同时允许几个线程执行。
- Semaphore字面意思是信号量的意思,它的作用是控制访问特定资源的线程数目。
构造方法:
public Semaphore(int permits):permits 表示许可线程的数量
重要方法:
public void acquire() throws InterruptedException: 表示获取许可
public void release(): release() 表示释放许可
演示:5名同学要进教室,但要设置每次只能2个同学进入教室
import java.util.concurrent.Semaphore;
class ClassRoom {
//最多2个线程进入
Semaphore sp = new Semaphore(2);
public void comeIn() {
// 获得许可
try {
sp.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 在教室
System.out.println(Thread.currentThread().getName() + ":获得许可,进入教室...");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":离开教室,释放许可...");
// 释放许可
sp.release();
}
}
public class Test {
public static void main(String[] args) {
// 创建ClassRoom对象
ClassRoom cr = new ClassRoom();
// 创建并启动线程
new Thread(new Runnable() {
@Override
public void run() {
cr.comeIn();
}
}, "张三1").start();
new Thread(new Runnable() {
@Override
public void run() {
cr.comeIn();
}
}, "张三2").start();
new Thread(new Runnable() {
@Override
public void run() {
cr.comeIn();
}
}, "张三3").start();
new Thread(new Runnable() {
@Override
public void run() {
cr.comeIn();
}
}, "张三4").start();
new Thread(new Runnable() {
@Override
public void run() {
cr.comeIn();
}
}, "张三5").start();
}
}
Exchanger类
Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange()方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。使用场景:可以做数据校对工作
构造方法:
public Exchanger()
重要方法:
public V exchange(V x): 传递数据,参数就是你要传递的数据,返回值就是其他线程传递给你的数据
代码示例
package demo12;
import java.util.concurrent.Exchanger;
class T1 extends Thread{
Exchanger<String> ex;
public T1(Exchanger<String> ex) {
this.ex = ex;
}
@Override
public void run() {
String s1 = null;
try {
s1 = ex.exchange("线程1的数据");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1 接收到线程2的数据是:"+s1);
}
}
class T2 extends Thread{
Exchanger<String> ex;
public T2(Exchanger<String> ex) {
this.ex = ex;
}
@Override
public void run() {
String s2 = null;
try {
s2 = ex.exchange("线程2的数据");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程2 接收到线程1的数据是:"+s2);
}
}
public class Test {
public static void main(String[] args) {
Exchanger<String> ex = new Exchanger<>();
new T1(ex).start();
new T2(ex).start();
}
}
|