目录
一. 线程安全问题
二. 你的多线程协调吗 —— synchronized
1. 同步代码块
2. 同步方法
三. 什么是易变数据 —— volatile
四. 要协调必须等待 —— wait方法
五. 你的线程协调得到通知了吗 —— notify 或 notifyAll
? ? ? ?在多线程的程序中,有多个线程并发运行,这多个并发执行的线程往往不是孤立的,它们之间可能会共享资源,也可能要相互合作完成某一项任务,如何使这多个并发执行的线程在执行的过程中不产生冲突,使多线程编程必须解决的问题。否则,可能导致线程运行的结果不正确,甚至造成死锁问题。
一. 线程安全问题
? ? ? ?在进行多线程的程序设计时,有时需要实现多个线程共享同一段代码,从而实现共享同一个私有成员或类的静态成员的目的。这时,由于线程和线程之间争抢CPU资源,线程无序地访问这些共享资源,最终可能导致无法得到正确的结果。这些问题通常称为线程安全问题。
? ? ? ?以火车站售票系统为例,在代码中判断当前票数是否大于0,如果大于0则执行将该票出售给乘客的功能,但当两个线程同时访问这段代码时(假如这时只剩下一张票),第一个线程将票售出,与此同时第二个线程也已经执行完成判断是否有票的操作,并得出票数大于0的结论,于是它也执行售出操作,这样就会产生负数。所以,在编写多线程程序时,应该考虑到线程安全问题。实质上线程安全问题来源于两个线程同时存取单一对象的数据。
? ? ? ?例如,在项目中创建ThreadSafeTest类,该类实现了Runnable接口,在未考虑到线程安全问题的基础上,模拟火车站售票系统的功能的代码如下:
public class ThreadSafeTest1 implements Runnable{
int num = 10;
@Override
public void run() {
while(true){
if(num > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->票数" + num--);
}
}
}
public static void main(String[] args) {
ThreadSafeTest1 t = new ThreadSafeTest1();
Thread t1 = new Thread("线程一");
Thread t2 = new Thread("线程二");
Thread t3 = new Thread("线程三");
Thread t4 = new Thread("线程四");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
-----------------------------------------------------------------------------------------
结果:
线程四-->票数10
线程三-->票数9
线程二-->票数8
线程一-->票数7
线程四-->票数6
线程三-->票数5
线程二-->票数4
线程一-->票数3
线程四-->票数2
线程三-->票数1
线程二-->票数-1
线程一-->票数0
线程四-->票数-2
? ? ? ?从这个结果可以看出,最后打印出的剩下的票数为负值,这样就出现了问题。这是由于同时创建了4个线程,这4个线程执行run()方法,在num变量为1时,线程一、线程二、线程三、线程四都对num变量有存储功能,当线程一执行run()方法时,还没有来得及做递减操作,就指定它调用sleep()方法进入就绪状态,这时线程二、线程三和线程四也都进入了run()方法,发现num变量依然大于0,但此时线程一休眠时间已到,将num变量值递减,同时线程二、线程三、线程四也都对num变量进行递减操作,从而产生了负值。
二. 你的多线程协调吗 —— synchronized
1. 同步代码块
? ? ? ?那么,该如何解决资源共享的问题呢?所有解决多线程资源冲突问题的方法基本上都是采用给定时间只允许一个线程访问共享资源的方法,这时就需要给共享资源上一道锁。这就好比一个人上洗手间时,他进入洗手间后会将门锁上,出来时再将锁打开,然后其他人才可以进入。
? ? ? ?Java中提供了同步机制,可以有效地防止资源冲突。同步机制使用synchronized关键字,使用该关键字包含的代码块称为同步块,也称为临界区,语法如下:
synchronized(synObject){
//关键代码
}
? ? ? ?通常将共享资源的操作放置在synchronized定义的区域内,这样当其他线程获取到这个锁时,就必须等待锁被释放后才可以进入该区域。Object为任意一个对象,每个对象都存在一个标志位,并具有两个值,分别为0和1。一个线程运行到同步块时首先检查该对象的标志位,如果为0状态,表明此同步块内存在其他线程,这时当期线程处于就绪状态,直到处于同步块中的线程执行完同步块中的代码后,这时该对象的标识位设置为1,当期线程才能开始执行同步块中的代码,并将Object对象的标识位设置为0,以防止其他线程执行同步块中的代码。
创建SynchronizedTest类,修改之前线程不安全的火车售票系统,把对num操作的代码设置在同步块中。
public class ThreadSafeTest1 implements Runnable{
int num = 10;
@Override
public void run() {
while(true){
synchronized (this){
if(num > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->票数" + num--);
}
}
}
}
public static void main(String[] args) {
ThreadSafeTest1 t = new ThreadSafeTest1();
Thread t1 = new Thread("线程一");
Thread t2 = new Thread("线程二");
Thread t3 = new Thread("线程三");
Thread t4 = new Thread("线程四");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
-----------------------------------------------------------------------------------------
结果:
线程一-->票数10
线程一-->票数9
线程一-->票数8
线程一-->票数7
线程一-->票数6
线程一-->票数5
线程一-->票数4
线程一-->票数3
线程一-->票数2
线程一-->票数1
从这个结果可以看出,打印到最后票数没有出现负数,这是因为将共享资源放置在了同步块中,不管程序如何运行都不会出现负数。
2. 同步方法
? ? ? ?同步方法就是在方法前面用synchronized关键字修饰的方法,其语法如下:
public synchronized void Main();
? ? ? ? 当某个对象调用了同步方法时,该对象上的其他同步方法必须等待该同步方法执行完毕后才能被执行。必须将每个能访问共享资源的方法修饰为synchronized,否则就会出错。
修改上面的代码,将共享资源操作放置在一个同步方法中,代码如下:
int num = 10;
public synchronized void doit(){
if(num > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->票数" + num--);
}
}
public void run(){
while(true){
doit();
}
}
三. 什么是易变数据 —— volatile
volatile 的定义:
? ? ? ?java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁更加方便。如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的.
Volatile是轻量级的synchronized,轻量级体现在一下几个方面:
- volatile?变量所需的编码较少
- 运行时开销也较少
- 不会引起线程上下文的切换和调度(线程上下文即线程的运行环境)
例1:
private volatile int num = 0;
private volatile String sum = nulll;
例2:假设多线程共同访问一个数组,即许多线程对这个数组进行排序,而同时其他线程打印这个数组的最小值和最大值。
...
static final int Size = 100;
static volatile int num[] = new int[Size];
static volatile int first = 0;
static volatile int last = 0;
static volatile boolean ready = false;
...
四. 要协调必须等待 —— wait方法
? ? ? ?wait() 方法和 notify() 或 notifyAll() 应当在 synchronized 的成程序块或者方法中配合使用,使多线程在共享资源和数据时得到进一步的保障。wait() 抛出检查性异常 InterruptedException。在一个?synchronized 的代码中调用 wait() 必须提供这个异常处理机制,例如:
try {
if (!ready){
wait();
...
}
}
...
? ? ? ?导致其他试图进入这个?synchronized 代码中的线程放弃监视器和锁定,保证只有当前线程执行这段协调代码。实际上,放弃锁定的其他所以线程都进入等待状态,直到某个在监视器中运行的线程调用?notify() 或者?notifyAll() ,例如:
if(ready){
notifyAll();
...
五. 你的线程协调得到通知了吗 —— notify 或 notifyAll
? ? ? ?notify() 和?notifyAll() 必须和 wait() 配合使用。notify() 只是唤醒一个正在等待的线程。如果代码中只有一个线程处于等待状态,调用?notify() 不存在问题。由于系统调度器处理线程调度安排的不透明性,唤醒哪个线程是不确定的。所以?notifyAll() 被经常使用,以增强等待线程被通知的可靠性。
?
|