Java 中多线程编程二: 多线程安全问题
I. 线程安全
1. 多线程的问题:
那么首先来看多线程为什么出现问题(出问题的标准):
是否是多线程环境
,因为单线程程序只要逻辑正确就没有问题是否有共享数据
, 没有共享数据的话,每个线程只访问和操作自己特有的数据,也不会出问题是否有多条语句操作共享数据
,- 因为 cpu 执行指令的原子性,cpu 一次执行一个原子性操作,比如一次加法或者一次赋值
- 如果有多个线程,每个线程对共享数据的操作只有一个原子性的操作,那么数据也不会出问题
- 但如果,每个线程中都有多条操作语句操作共享数据,比如
A;B;C;
三个语句,对共享数据进行操作,理想顺序是线程1执行完 ABC,线程2再执行ABC - 但是由于多线程执行的随机性,有可能线程1执行到B的时候,线程1就执行了A,那么得到的值就和期待值不同
- 于是多线程问题就出现了
所以程序中,满足了上面三个条件,就存在线程的安全问题的!
2. 解决线程安全问题的基本思想
当程序满足了上面三个条件,出现了安全问题,那么如何解决呢?
- 基本思想:
前两个条件一般是不能进行更改的,所以要从第三个条件入手。如果我们把多条语句操作共享数据的代码给包装成一个整体,比如 A; B; C;
三个语句,我们让一个线程在执行这三个语句的时候,一次性把他们当作一个语句执行了。就是说,线程1在执行这个语句的整体的时候,线程2就不会去执行这个包装好的整体语句。所以就不会出现线程1才执行到 B 语句,线程二就执行到A语句的情况了。
- 怎么实现:Java 给我们提供了
同步机制
3. 同步代码块
- 格式:
synchronized(对象){需要同步的代码}
- 这个对象:就是一个锁对象,用这个锁锁住所有需要操作共享数据的线程
- 需要同步的代码:把多条语句操作共享数据的代码的部分给包起来
- 同步可以解决安全问题的根本原因就在那个对象上,该对象如同锁的功能
- 多个线程必须是同一把锁
package org.lovian.thread.synchronize;
public class SellTicket implements Runnable {
// share area of threads
private int tickets = 100;
// lock object
private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) { // the node need to be synchronized
if (tickets > 0) {
try {
Thread.sleep(100); // simulate of the network delay
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " is selling No. " + (tickets--) + " ticket");
}
}
}
}
}
package org.lovian.thread.synchronize;
public class SellTicketDemo {
public static void main(String[] args) {
SellTicket sellTicket = new SellTicket();
Thread window1 = new Thread(sellTicket,"Window 1");
Thread window2 = new Thread(sellTicket,"Window 2");
Thread window3 = new Thread(sellTicket,"Window 3");
window1.start();
window2.start();
window3.start();
}
}
上述代码中,用 synchronized
关键字把操作共享数据的代码区域锁住了,而 syncronized 需要一个对象,如果这个对象不在共享区,那么就相当于每一个线程都有一把锁,这是没有意义的。锁
在有一个线程正在执行同步代码块的时候,把这个同步代码块锁住,如果这时候有另一个线程抢到了cpu的执行权,但是发现这里被锁住了,它就只能等着锁住的代码执行结束,然后锁打开,它才能进去执行代码块中的代码
通俗的举例,相当于在火车上上厕所,每个人相当于一个线程,上厕所的操作相当于同步代码块,厕所门锁就是那个同步 object。所以当一个人抢到了厕所的使用权,上厕所,锁住门,另一个人就没法进去;当一个人上完了,另一个人才能抢到厕所的使用权
4.同步的特点
- 前提条件:
- 有多个线程
- 多个线程必须使用同一把锁
- 同步的好处:
- 解决了多线程的安全问题
- 同步的弊端:
- 当线程很多时,每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
- 同步代码块的锁对象: 是任意对象
5. 同步方法的格式及锁对象问题
- 同步方法: 把
synchronized
关键字加在方法的声明上 - 同步方法的锁对象:
this
- 静态方法锁对象: 类的字节码文件对象,
当前类.class
, 因为静态内容是随着 class 文件被加载到方法区的时候而加载,所以这个对象就是这个类的字节码文件对象
我们把上面例子的代码改成下面使用同步方法的代码
package org.lovian.thread.synchronize;
public class SellTicket implements Runnable {
// 1000 tickets
private int tickets = 100;
// lock object
private Object obj = new Object();
@Override
public void run() {
while (true) {
sellTikets();
}
}
//同步方法
private synchronized void sellTikets() {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " is selling No. " + (tickets--) + " ticket");
}
}
}
II. Java 中线程安全的类
我们知道 Java 中有一些类是线程安全的,比如 StringBuffer
, Vector
, 和 Hashtable
, 他们中的成员方法大多数都被 synchronized
标注了。但是安全的同时,效率会更低。
即使我们有 Vector
和 Hashtable
, 但是实际上开发的时候,也不会去用它们,而是使用 Collections
工具类提供的返回线程安全的 list 集合等,比如:
public static <T> List<T> synchronizedList(List<T> list)
: 返回指定列表支持的同步(线程安全的)列表public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m)
: 返回由指定映射支持的同步(线程安全的)映射public static <T> Set<T> synchronizedSet(Set<T> s)
:返回指定有序 set 支持的同步(线程安全的)有序 set
III. JDK5 中 Lock 锁的使用
为了清晰的表达如何加锁和释放锁,JDK5 以后提供了一个新的锁对象 Lock
, 有了它以后,我们就可以明确的知道在哪里加了锁,在哪里释放了锁
1. Lock 接口
java.util.concurrent.locks.Lock:
Lock 实现提供了比使用 synchronized
方法和语句可获得的更广泛的锁定操作
void lock()
: 获取锁void unlock()
: 释放锁
2. Lock 接口主要实现类
java.util.concurrent.locks.ReentrantLock:
一个可重入的互斥锁,比 synchronized 功能更强大,使用 lock()
方法在需要同步的代码块的位置上加锁,使用 unlock
在同步代码块结束的位置上解锁。
package org.lovian.thread.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SellTicket implements Runnable {
private int tickets = 100;
// define a lock
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
lock.lock(); // add lock
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " is selling No. " + (tickets--) + " ticket");
}
} finally {
// release lock anyway
lock.unlock();
}
}
}
}
这里使用了 try/finally
语句块,是因为无论同步代码块有没有出问题,锁都是要释放的
3. 死锁问题
- 同步的弊端:
- 效率低
- 如果出现了同步嵌套,就容易产生死锁问题
死锁问题
:- 是指两个或者两个以上的线程在执行的过程中, 因争夺资源产生的一种互相等待的现象
死锁的代码示例:
package org.lovian.thread.lock;
public class MyLock {
// create two locks
public static final Object LOCK_A= new Object();
public static final Object LOCK_B = new Object();
}
package org.lovian.thread.lock;
public class DieLock extends Thread {
private boolean flag;
public DieLock(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
synchronized (MyLock.LOCK_A) {
System.out.println("if LOCK_A");
synchronized (MyLock.LOCK_B) {
System.out.println("if LOCK_B");
}
}
} else {
synchronized (MyLock.LOCK_B) {
System.out.println("else LOCK_B");
synchronized (MyLock.LOCK_A) {
System.out.println("else LOCK_A");
}
}
}
}
}
package org.lovian.thread.lock;
public class DieLockDemo {
public static void main(String[] args) {
DieLock dl1 = new DieLock(true);
DieLock dl2 = new DieLock(false);
dl1.start();
dl2.start();
}
}
result:
if LOCK_A
else LOCK_B
从结果我们可以看出,程序卡在这里了,线程1执行了 if 语句块中的 A 锁,而线程2 执行了 else 语句块的 B 锁, 所以线程1 在等 B 锁的释放,而线程2在等 A 锁的释放,结果两个线程都卡在这里了,造成了死锁
IV. 线程间通信
线程间通信问题,其实就是不同种类的线程之间针对一个资源的操作。
举个例子:
早餐店卖包子,至少有做新包子和顾客买包子两种操作,那么包子就是资源,两个线程一个增加资源,一个减少资源。所以线程间通信,其实就是通过设置线程
(生产者)和获取线程
(消费者)针对同一个对象进行操作。
1.代码示例
我们以学生类(student)作为资源类来进行线程间通信的演示,首先分析关系:
- 资源类: Student
- 设置学生数据: SetThread (设置线程,生产者)
- 获取学生数据: GetThread (获取线程,消费者)
- 测试类: StudentDemo
Student 类代码:
package org.lovian.thread.communication;
public class Student {
private String name;
private int age;
...
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
...
}
SetThread 类代码:
package org.lovian.thread.communication;
public class SetThread implements Runnable {
private Student s;
private int count = 0;
public SetThread(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
synchronized (s) {
if (count % 2 == 0) {
s.setName("James");
s.setAge(25);
} else {
s.setName("Lili");
s.setAge(18);
}
}
count++;
}
}
}
GetThread 类代码:
package org.lovian.thread.communication;
public class GetThread implements Runnable {
private Student s;
public GetThread(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
synchronized (s) {
System.out.println(s.getName() + " -- " + s.getAge());
}
}
}
}
测试类:
package org.lovian.thread.communication;
public class StudentDemo {
public static void main(String[] args) {
// 创建资源
Student s = new Student();
// 设置线程
SetThread st = new SetThread(s);
Thread t1 = new Thread(st, "SetStudent");
// 获取线程
GetThread gt = new GetThread(s);
Thread t2 = new Thread(gt, "GetStudent");
t1.start();
t2.start();
}
}
result:
null -- 0
null -- 0
...
Lili -- 18
Lili -- 18
Lili -- 18
...
注意:
- 资源类 student 的对象 s, 必须被设置线程和获取线程共有
- 通过构造器参数传入
- 通过 set 方法
- 由于现在资源类对象 s 被两个线程同时拥有,而且还同时被两个线程操作,就会出现线程安全问题
- 设置线程和获取线程要同时上锁
- 两个线程必须上同一把锁,这里的锁就是传入的资源类 Student 的对象 s
- 问题:
- 如结果所示,获取线程可能在设置线程之前得到了 CPU 的执行权,所以结果为 null – 0
- 而且可能每个线程都连续的获得了 CPU 的执行权,无论是生产者还是消费者,这都是不合理的
- 解决问题思路:
- 生产者:先看是否有资源,有就等待,没有就生产,生产完通知消费者来消费
- 消费者:先看是否有资源,有就消费,没有就等待,通知生产者生产数据
- 为了解决这个问题,java 提供了
等待唤醒机制
2.等待唤醒机制
等待唤醒机制
使用了 Object
类中提供的方法:
wait()
: 在其他线程调用此对象的 notify 方法时, 导致当前线程等待,等待的时候,会将锁释放notify()
: 唤醒此对象监视器上等待的单个线程notifyAll()
: 唤醒此对象监视器上等待的所有线程
为什么这些方法不定义在Thread
类中呢?
因为,这些方法应该是锁对象
来调用的,然而在多线程中,锁对象可能是任意一个对象,所以,就把方法定义在所有类的超类 Object 里。
等待唤醒机制思想:
- 生产者:先看是否有资源,有就等待,没有就生产,生产完通知消费者来消费
- 消费者:先看是否有资源,有就消费,没有就等待,通知生产者生产数据
- 线程执行时,要保证必须先有资源
那么应用等待唤醒机制,修改代码:
Student类:
package org.lovian.thread.communication.waitnotify;
public class Student {
private String name;
private int age;
private boolean flag; // 是否有数据,有 true, 没有 false
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
生产者 SetThread 类:
package org.lovian.thread.communication.waitnotify;
public class SetThread implements Runnable {
private Student s;
private int count = 0;
public SetThread(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
synchronized (s) {
if(s.isFlag()){ // 判断是否有资源 --> 有
try {
s.wait(); // 生产者线程等待,释放锁;被唤醒后,生产者继续从这里执行
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (count % 2 == 0) {
s.setName("James");
s.setAge(25);
} else {
s.setName("Lili");
s.setAge(18);
}
count++;
// 生产资源了,改标记
s.setFlag(true);
// 唤醒消费者线程
s.notify();
}
}
}
}
消费者 GetThread 类:
package org.lovian.thread.communication.waitnotify;
public class GetThread implements Runnable {
private Student s;
public GetThread(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
synchronized (s) {
if(!s.isFlag()){ // 判断是否有资源 --> 没有
try {
s.wait(); // 消费者等待,释放锁;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(s.getName() + " -- " + s.getAge());
// 消费完资源,修改标记
s.setFlag(false);
// 唤醒生产者线程
s.notify();
}
}
}
}
测试类代码不变, 结果:
James -- 25
Lili -- 18
James -- 25
Lili -- 18
James -- 25
Lili -- 18
...
从结果可以看见这里生产者线程和消费者线程就交替执行了。注意,在 wait()
方法被执行时,会同时释放锁,让另一个拥有此锁的线程可以被执行。 当线程被唤醒时,代码从 wait()
代码处继续开始执行,但是,被唤醒后,线程不是立刻执行的
,它还需要抢夺 CPU 的执行权。
修改版代码:
package org.lovian.thread.communication.waitnotify;
public class Student {
private String name;
private int age;
private boolean flag; // 是否有数据,有 true, 没有 false, 默认 false
public synchronized void set(String name, int age) {
if (this.flag) { // 有数据
try {
this.wait(); // 生产者等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 没有数据, 生产数据
this.name = name;
this.age = age;
// 修改标记,唤醒消费者线程
this.flag = true;
this.notify();
}
public synchronized void get() {
if (!this.flag) { // 没有数据
try {
this.wait(); // 消费者等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 有数据,消费数据
System.out.println("name: " + this.name + " age: " + this.age);
// 修改标记, 唤醒生产者去生产
flag = false;
this.notify();
}
}
package org.lovian.thread.communication.waitnotify;
public class SetThread implements Runnable {
private Student s;
private int count = 0;
public SetThread(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
if (count % 2 == 0) {
s.set("James", 25); // 生产者生产数据
} else {
s.set("Lili", 18); // 生产者生产数据
}
count++;
}
}
}
package org.lovian.thread.communication.waitnotify;
public class GetThread implements Runnable {
private Student s;
public GetThread(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
s.get(); //消费者获取数据
}
}
}
这个版本代码,将设置(生产者生产)和获取(消费者消费)的操作封装成学生类(资源)里的功能,并加了同步,这是消费者线程和生产者线程只需要调用方法即可,代码更为简洁,耦合度更低。
3. 线程状态转换图
V. 线程组
Java 中用 java.lang.ThreadGroup
来表示线程组:
- 它可以对一批线程进行分类管理, Java允许程序直接对线程组进行控制
- 默认情况下,所有的线程都属于主线程组
- 获取线程组:
public final ThreadGroup getThreadGroup()
- 设置线程组:
Thread(ThreadGroup group, Runnable target, String threadName)
我们可以用 ThreadGroup 来把线程分到特定的组里,然后通过这个组,统一的进行线程的操作:
public final void setDaemon(boolean daemon)
: 将线程组中的线程全部设置成守护线程public final void setMaxPriority(int pri)
: 设置线程组中线程最大的优先级(应该在 [Thread.MIN_PRIORITY, Thread.MAX_PRIORITY] 范围中)public final void interrupt()
: 中断此线程组中的所有线程
package org.lovian.thread.group;
public class ThreadGroupDemo {
public static void main(String[] args) {
System.out.println("Get ThreadGroup Demo:");
getThreadGroupDemo();
System.out.println("====================");
System.out.println("Set ThreadGroup Demo: ");
setThreadGroupDemo();
}
private static void getThreadGroupDemo(){
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr, "T1");
Thread t2 = new Thread(mr, "T2");
// Get thread group of threads
ThreadGroup tg1 = t1.getThreadGroup();
ThreadGroup tg2 = t2.getThreadGroup();
// Get the name of thread group
String name1 = tg1.getName();
String name2 = tg2.getName();
System.out.println("Tg1: " + name1 + " Tg2: " + name2); // default is 'main'
System.out.println("main thread: " + Thread.currentThread().getThreadGroup().getName());
}
private static void setThreadGroupDemo(){
// Change the thread group of threads
// new a ThreadGroup
ThreadGroup tg = new ThreadGroup("Group_1");
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(tg, mr, "T1");
Thread t2 = new Thread(tg, mr, "T2");
// Get the name of thread group
System.out.println("Tg1: " + t1.getThreadGroup().getName() + " Tg2: " + t2.getThreadGroup().getName());
System.out.println("main thread: " + Thread.currentThread().getThreadGroup().getName());
}
}
result:
Get ThreadGroup Demo:
Tg1: main Tg2: main
main thread: main
====================
Set ThreadGroup Demo:
Tg1: Group_1 Tg2: Group_1
main thread: main
Share this on