多线程线程安全问题
- 1 什么是线程安全
- 2 线程安全解决办法
- 3 synchronized内置的锁
- 3.1 synchronized的俩种使用方式
- 3.1.1 同步代码块方式
- 3.1.2 同步方法方式
- 4 多线程死锁
- 4.1 什么是多线程死锁
- 4.2 产生死锁的四个必要条件
- 4.3 避免死锁的方式
- 5 Threadlocal
- 5.1 什么是Threadlocal
- 5.2 ThreadLocal的接口方法
- 5.3 ThreadLocal代码演示
- 5.4 ThreadLocal实现原理
- 6多线程有三大特性
- 6.1 原子性
- 6.2 可见性
- 6.3 有序性
- 7.多线程如何保证三大特性呢
- 7.1 保证原子性
- 7.2 保证可见性
- 7.3 保证有序性
- 7.4 什么是happens-before原则
1 什么是线程安全
当多个线程同时共享,同一个全局变量或静态变量,做写的操作时,可能会发生数据冲突问题,也就是线程安全问题。但是做读操作是不会发生数据冲突问题。
案例:需求现在有100张火车票,有三个窗口同时抢火车票,请使用多线程模拟抢票效果。
package com.lijie;
public class ThreadTrain implements Runnable {
private int trainCount = 100;
@Override
public void run() {
while (trainCount > 0) {
try {
Thread.sleep(50);
} catch (Exception e) {
}
sale();
}
}
public void sale() {
if (trainCount > 0) {
System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - trainCount + 1) + "张票");
trainCount--;
}
}
public static void main(String[] args) {
ThreadTrain threadTrain = new ThreadTrain();
Thread t1 = new Thread(threadTrain, "1号");
Thread t2 = new Thread(threadTrain, "2号");
Thread t3 = new Thread(threadTrain, "3号");
t1.start();
t2.start();
t3.start();
}
}
运行结果:
结论发现,多个线程共享同一个全局成员变量时,做写的操作可能会发生数据冲突问题。
2 线程安全解决办法
使用多线程之间同步synchronized或使用锁(lock)。
为什么使用线程同步或使用锁能解决线程安全问题呢
将可能会发生数据冲突问题,只能让当前一个线程进行执行。代码执行完成后释放锁,让后才能让其他线程进行执行。这样的话就可以解决线程不安全问题。
3 synchronized内置的锁
Java提供了一种内置的锁机制来支持原子性:synchronized关键字
synchronized称为内置锁,当线程进入同步代码块之前自动获取到锁,代码块执行完成正常退出或代码块中抛出异常退出时会释放掉锁
即:线程A获取到锁后,线程B阻塞直到线程A释放锁,线程B才能获取到同一个锁
3.1 synchronized的俩种使用方式
3.1.1 同步代码块方式
//就是将可能会发生线程安全问题的代码,给包括起来。
synchronized(对象)//这个对象可以为任意对象
{
需要被同步的代码
}
对象如同锁,持有锁的线程可以在同步中执行 ,没持有锁的线程即使获取CPU的执行权,也进不去
同步的前提:
1,必须要有两个或者两个以上的线程
2,必须是多个线程使用同一个锁
3,必须保证同步中只能有一个线程在运行
好处:解决了多线程的安全问题
弊端:多个线程需要判断锁,较为消耗资源、抢锁的资源。
代码演示:
修改上方抢票代码的sale方法
public void sale() {
synchronized (this) {
if (trainCount > 0) {
System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - trainCount + 1) + "张票");
trainCount--;
}
}
}
此时不会出现数据冲突问题
3.1.2 同步方法方式
在方法上修饰synchronized 称为同步方法
代码演示:
//方法上添加synchronized 关键字即可
public synchronized void sale() {
if (trainCount > 0) {
System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - trainCount + 1) + "张票");
trainCount--;
}
}
4 多线程死锁
4.1 什么是多线程死锁
例如:对象A持有一把锁,他需要使用B中的代码,结果B也持有一把锁,他同时要使用A中的代码。这是就会发生死锁
在Java中使用多线程,就会有可能导致死锁问题。死锁会让程序一直卡住,不再程序往下执行。我们只能通过中止并重启的方式来让程序重新执行。
互相加锁引用 引起的死锁代码演示:
package com.lijie;
public class DeadLock {
public static String obj1 = "obj1";
public static String obj2 = "obj2";
public static void main(String[] args) {
Thread a = new Thread(new Lock1());
Thread b = new Thread(new Lock2());
a.start();
b.start();
}
}
class Lock1 implements Runnable {
@Override
public void run() {
try {
System.out.println("Lock1 running");
while (true) {
synchronized (DeadLock.obj1) {//加锁
System.out.println("Lock1 lock obj1");
Thread.sleep(3000);//获取obj1后先等一会儿,让Lock2有足够的时间锁住obj2
synchronized (DeadLock.obj2) {//应用第二个加锁
System.out.println("Lock1 lock obj2");
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class Lock2 implements Runnable {
@Override
public void run() {
try {
System.out.println("Lock2 running");
while (true) {
synchronized (DeadLock.obj2) {//加锁
System.out.println("Lock2 lock obj2");
Thread.sleep(3000);//获取obj2后先等一会儿,让Lock1有足够的时间锁住obj1
synchronized (DeadLock.obj1) {//应用第一个加锁
System.out.println("Lock2 lock obj1");
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
4.2 产生死锁的四个必要条件
- 互斥条件:在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,就只能等待,直至占有资源的进程用毕释放。
- 占有且等待条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
- 不可抢占条件:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。(比如一个进程集合,A在等B,B在等C,C在等A)
4.3 避免死锁的方式
- 避免一个线程同时获得多个锁
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
- 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制
5 Threadlocal
5.1 什么是Threadlocal
ThreadLocal提高一个线程的局部变量,访问某个线程拥有自己局部变量。
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
5.2 ThreadLocal的接口方法
ThreadLocal类接口很简单,只有4个方法:
- void set(Object value)设置当前线程的线程局部变量的值。
- public Object get()该方法返回当前线程所对应的线程局部变量。
- public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
- protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
5.3 ThreadLocal代码演示
案例:创建三个线程,每个线程生成自己独立序列号。
package com.lijie;
class Res {
// 生成序列号共享变量
public static Integer count = 0;
//创建ThreadLocal变量
public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
protected Integer initialValue() {
return 0;
}
;
};
//每次获取序列号+1
public Integer getNum() {
int count = threadLocal.get() + 1;
threadLocal.set(count);
return count;
}
}
public class ThreadLocaDemo extends Thread {
private Res res;
public ThreadLocaDemo(Res res) {
this.res = res;
}
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + "---" + "i---" + i + "--num:" + res.getNum());
}
}
public static void main(String[] args) {
Res res = new Res();
ThreadLocaDemo threadLocaDemo1 = new ThreadLocaDemo(res);
ThreadLocaDemo threadLocaDemo2 = new ThreadLocaDemo(res);
ThreadLocaDemo threadLocaDemo3 = new ThreadLocaDemo(res);
threadLocaDemo1.start();
threadLocaDemo2.start();
threadLocaDemo3.start();
}
}
5.4 ThreadLocal实现原理
ThreadLocal底层通过map集合,Map.put(“当前线程”,值);
6多线程有三大特性
原子性、可见性、有序性
话说多线程编程必须要保证原子性、可见性以及有序性,缺一不可,不然就可能导致结果执行不正确。(我个人决定其实很多情况并不一定是需要保证三大特性的)
6.1 原子性
即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
一个很经典的例子就是银行账户转账问题:
比如:从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。这2个操作必须要具备原子性才能保证中间不出现一些意外的问题。
6.2 可见性
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
若两个线程在不同的cpu,那么线程1改变了i的值还没刷新到主存,线程2又使用了i,那么这个i值肯定还是之前的,线程1对变量的修改线程没看到这就是可见性问题。
6.3 有序性
程序执行的顺序按照代码的先后顺序执行。
一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
int a = 5; //语句1
int r = 3; //语句2
a = a + 2; //语句3
r = a*a; //语句4
则因为重排序,他还可能执行顺序为(这里标注的是语句的执行顺序) 2-1-3-4,1-3-2-4
但绝不可能 2-1-4-3,因为这打破了依赖关系。
显然重排序对单线程运行是不会有任何问题,但是多线程就不一定了,所以我们在多线程编程时就得考虑这个问题了。
7.多线程如何保证三大特性呢
话说多线程编程必须要保证原子性、可见性以及有序性,缺一不可,不然就可能导致结果执行不正确。(我个人决定其实很多情况并不一定是需要保证三大特性的)
7.1 保证原子性
保证多线程原子性很简单,使用synchronized或者lock锁来保证原子性
7.2 保证可见性
用好volatile关键字即可保证可见性(下一章会讲到)
7.3 保证有序性
线程一旦多起来保证有序性就很麻烦了。Java中可通过volatile在一定程序上保证顺序性,另外还可以通过synchronized和lock锁和保证原子性一样的来保证顺序性。 还可以利用ThreadLocal,让访问某个线程拥有自己局部变量巧妙的来保证。
还有除了从应用层面保证目标代码段执行的顺序性外,JVM还通过被称为happens-before原则隐式地保证顺序性。
7.4 什么是happens-before原则
简单来说就是:前一个操作的结果可以被后续的操作获取。讲白点就是前面一个操作把变量A赋值为1,那后面一个操作肯定能知道A已经变成了1。