学习内容来源于蚂蚁课堂,感谢蚂蚁课堂大佬的指导,本文仅供学习记录使用,如有不适,请联系作者。
一、什么是线程安全问题
多个线程共享同一个全局变量,做写操作的时候,可能会受到其他线程的干扰(局部变量不会),导致数据有问题,做读操作的时候不会产生线程安全问题。
/**
*多线程安全问题案例
*有100张火车票,两个窗口同时抢票
*/
public class Demo01 {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable ();
Thread t1 = new Thread(myRunnable,"窗口1");
Thread t2 = new Thread(myRunnable,"窗口2");
t1.start();
t2.start();
}
}
//继承Thread
class MyRunnable implements Runnable{
//模拟同一变量写操作
private int trainCount = 100;
/**
* run方法就是线程要执行的代码
*/
@Override
public void run() {
/*
*问题点出在while条件中,当trainCount = 1时
*一个线程进入,并睡眠,另一个线程也进入while条件
*导致多售出一张票
*/
while (trainCount > 0) {
try {
Thread.sleep(50);
sale();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public void sale() {
System.out.println(Thread.currentThread().getName() + "出售第:" + (100-trainCount + 1) + "票");
trainCount--;
}
}
//输出,显而易见多售出了一张票,并且窗口1,2都出售了第一张票
//出现问题的原因就是多个线程操作同一全局变量trainCount
窗口2出售第:1票
窗口1出售第:1票
...省略
窗口1出售第:97票
窗口2出售第:98票
窗口1出售第:99票
窗口2出售第:100票
窗口1出售第:101票
二、线程安全问题解决办法
线程之间实现同步(保证数据的原子性,即数据不收其他线程干扰,不要想成同步和异步的区别)
2.1自动(不用考虑释放锁): synchronized
public class Demo02 {
public static void main(String[] args) {
CreateThreadDemo02 thread = new CreateThreadDemo02();
Thread t1 = new Thread(thread,"窗口1");
Thread t2 = new Thread(thread,"窗口2");
t1.start();
t2.start();
}
}
//继承Thread
class CreateThreadDemo02 implements Runnable{
private int trainCount = 100;
//这里以obj作为锁
private Object obj = new Object();
/**
* run方法就是线程要执行的代码
*/
@Override
public void run() {
while (trainCount > 0) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
sale();
}
}
/*同步函数:synchronized 也可加在方法上
* 同步函数使用的是this锁
*public synchronized void sale() {
*/
public void sale() {
synchronized (obj) { //同步代码块:只有一个线程能进行访问,必须拿到锁才能访问
if (trainCount > 0) {
System.out.println(Thread.currentThread().getName() + "出售第:" + (100-trainCount + 1) + "票");
trainCount--;
}
}
}
}
窗口1出售第:1票
窗口2出售第:2票
...省略
窗口1出售第:97票
窗口2出售第:98票
窗口1出售第:99票
窗口2出售第:100票
2.1.1 synchronized什么地方考虑加锁?
在真正对同一全局变量操作的地方上锁,最好不要在整段代码上上锁,影响效率,考虑真正可能出现线程安全问题的地方。
2.1.2 线程安全问题解决思路
不要多个线程同时对一个全局变量写操作
2.1.3 加锁的条件
- 必须两个线程以上,需要发生同步(保证数据之间的原子性)。
- 多个线程想同步,必须用同一把锁。
- 保证只有一个线程执行。
2.1.4 原理
有一个线程已经拿到了锁,其他线程已经有cpu执行权了,也要一直排队,等待其他线程释放锁。
锁释放:代码执行完毕,或者是程序抛出异常。
2.1.5 缺点
效率非常低,锁的资源竞争,一直不释放锁,会造成死锁。
2.1.6 同步函数
- 静态同步函数(static synchronized): 使用的是当前字节码文件 xxx.class。
- 非静态同步函数: 使用this锁。
2.1.7 多线程死锁
同步中嵌套同步,互相无法释放,一直…一直等待变为死锁
/**
*
* 例:
* 线程1 先拿到同步带码块obj锁,再拿到同步函数this锁
* 线程2 先再拿到同步函数this锁,再到同步带吗块obj锁,
*/
public class Demo05 {
public static void main(String[] args) {
CreateThreadDemo04 thread = new CreateThreadDemo04();
Thread t1 = new Thread(thread,"窗口①");
Thread t2 = new Thread(thread,"窗口②");
t1.start();
try {
Thread.sleep(40);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
thread.flag = false;
t2.start();
}
}
//继承Thread
class CreateThreadDemo04 implements Runnable{
private int trainCount = 100;
private Object obj = new Object();
public boolean flag = true;
/**
* run方法就是线程要执行的代码
*/
@Override
public void run() {
if (flag) {
while (true) {
synchronized (obj) {
sale();
}
}
}else {
while (true) {
sale();
}
}
}
public synchronized void sale() {
synchronized (obj) {
if (trainCount > 0) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "出售第:" + (100 - trainCount + 1) + "票");
trainCount--;
}
}
}
}
2.2手动(自己上锁解锁): lock—jdk1.5
后续详细讲解。
三、java内存模型
简单介绍,后续详细介绍。
3.1多线程的三大特性
- 原子性:保证线程独一无二,保证线程安全问题。
- 可见性:java内存模型。
- 有序性:join方法,wait,notify (多线程之间通讯)。
3.2 java内存模型
属于多线程可见性问题 简称jmm,区别于java内存结构,属于jvm内存分配决定了一个线程与另一个线程是否可
- 主内存: 主要存放共享的全局变量
- 私有本地内存:本地线程私有变量
四、Volatile关键字
保证线程之间可见但不保证原子性(不保障线程安全问题)
public class Demo07 {
public static void main(String[] args) throws InterruptedException {
ThreadVolatileDemo threadVolatileDemo = new ThreadVolatileDemo();
threadVolatileDemo.start();
//Thread t1 = new Thread();
//休眠,使主线程延迟刷新到主内存,通知其他子线程
Thread.sleep(3000);
// 主线程修改了共享的全局变量为false
threadVolatileDemo.setFlag(false);
System.out.println("flag值已被修改为false");
Thread.sleep(3000);
System.out.println(threadVolatileDemo.flag);
}
}
class ThreadVolatileDemo extends Thread{
public volatile boolean flag = true;
//public boolean flag = true;
@Override
public void run() {
System.out.println("子线程开始执行");
while(flag) {
}
System.out.println("子线程执行结束");
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
//输出
子线程开始执行
flag值已被修改为false
子线程执行结束
false
//如果去掉volatile关键字
//输出如下,子线程不会结束
子线程开始执行
flag值已被修改为false
false
五、AtomicInteger原子类
主要做计数使用,解决同步问题,保证了线程安全
/**
*错误代码演示
*/
public class Demo08_Error extends Thread{
private static int count = 0;
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
count ++;
}
System.out.println(getId() + ":" + count);
}
public static void main(String[] args) {
//创建10个线程
Demo08_Error[] array = new Demo08_Error[10];
for (int i = 0; i < array.length; i++) {
array[i] = new Demo08_Error();
}
for (int i = 0; i < 10; i++) {
array[i].start();
}
}
}
//输出
10:1000
14:3216
18:7220
16:7220
12:6220
13:8268
15:6220
17:5000
11:4000
19:9220
import java.util.concurrent.atomic.AtomicInteger;
/**
*正确代码演示
*/
public class Demo08 {
public static void main(String[] args) {
VolatileNoAtomic[] array = new VolatileNoAtomic[10];
for (int i = 0; i < array.length; i++) {
array[i] = new VolatileNoAtomic();
}
for (int i = 0; i < 10; i++) {
array[i].start();
}
}
}
class VolatileNoAtomic extends Thread{
//private static int count = 0;
private static AtomicInteger count = new AtomicInteger(0);
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
count.incrementAndGet();
}
System.out.println(getName() + ": " + count.get());
}
}
//输出
Thread-0: 2427
Thread-1: 3000
Thread-2: 3000
Thread-5: 5000
Thread-6: 6000
Thread-9: 7000
Thread-4: 7000
Thread-3: 8000
Thread-7: 9878
Thread-8: 10000