JAVA多线程编程(二)——同步与通信
同步是因为多线程之间共享数据访问(访问包括数据的读和写)时产生了数据安全问题。同步的问题,在嵌入式ROTS当中称为共享资源的互斥访问,可以用信号量(semaphore)或 互斥锁(mutex)来保护共享资源。
JAVA多线程解决此类问题的方法是用synchornized进行同步保护,同步可以同步代码块,也可以同步非静态的方法,还可以同步静态的方法。
1. 同步代码块
使用方法:synchornized(Object 对像) { 需要同步的代码块 }
注意两个问题:
A. 同步的对像是谁?答:任意对像!
B. 多个线程需要同步的是同一个对像!!!
2. 同步非静态的方法
使用方法: public synchronized 【返回值类型】 方法名() { }
同步方法时并未显式的指明同步对像,那它同步的是谁???
答: this对像。
3. 同步静态方法
锁对像是谁?this? 静态随着类的加载而加载,此时还没有this!!!这里的锁对象是当前类的字节码文件对象(反射再讲字节码文件对象)
当然,现在的JAVA中还增加了ReentrantLock类,synchronized关键字自动提供了一个锁及相关的“条件”,但是不直观,而用Lock可以显式的指明在哪里锁上,又在哪里开锁!
一、同步问题解决方法之——synchronized关键字
典型案例分析,同步问题的产生!
解
决方法一:用extends Thread方法创建多线程,解决线程安全问题。
package Thread;
import java.lang.Thread;
//第一种方式创建多线程
class MyThread extends Thread{
//static修饰为共享数据,否则每个线程有各自的ticks
public static int ticks = 100;
//static修饰为共享对像,否则每个线程有各自的obj对像
//synchronized同步的对像是不同的,同步无效!!!
public static Object obj = new Object();
public void run()
{
while(ticks > 0)
{
//同步代码块
synchronized(obj)
{
System.out.println(Thread.currentThread().getName() + " ==== " + ticks --);
}
try
{
Thread.sleep(10);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}
public class ThreadTest{
public static void main(String[] args)
{
//创建多线程
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
//启动多线程
t1.start();
t2.start();
t3.start();
}
}
经测试,上面方法,共享的数据一定要加static修饰,不然每个线程有各自的成员属性和成员方法,互不影响,而同步的对像一定也要用static修饰,理由一样,多个对像共享一个静态的成员!
更不能用: synchronized( new Object() )这种方法去同步,因为每次同步的对像都是新创建的!!!
同时需要注意:同步代码块的同步范围问题,只要发生互斥访问(包括读和写),都需要同步。
解决方法
二:用implements Runnable方法创建多线程,解决线程安全问题。
package Thread;
class MyThread1 implements Runnable{
//实现Runnable接口的方法创建多线程不用加static
//因为所有的线程共享一份数据和代码
public int ticks = 100;
public Object obj = new Object();
public void run()
{
while(ticks > 0)
{
//同步代码块,读和写都要同步进来
//不能只同步synchronized(obj){ ticks --}
//因为在一个线程把数据读出(把X号票卖出)后还未进行tick--(剩余票数未更新)
//执行权便被剥夺,其它线程获得执行权访问的并不是最新的数据,所以还会再把X号票卖出
synchronized(obj)
{
System.out.println(Thread.currentThread().getName() + " ==== " + ticks --);
}
try
{
Thread.sleep(10);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}
public class ThreadTest1
{
public static void main(String[] args)
{
//自定义线程,实现Runnable接口
MyThread1 t = new MyThread1();
//创建线程并把实现Runnable接口的类的对像作为参数传递
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
//启动线程
t1.start();
t2.start();
t3.start();
}
}
附加问题:同步代码块和同步方法同时运用!
因为线程同步靠的是检查同一对像的锁标志位,只要让同步代码块和同步方法使用同一个监视器对像,那就能很好的同步,在同一个类的成员中,同步代码块的队像设为this就可以和其它同步的成员方法互斥访问共享资源了。
二、同步问题解决方法二——ReentrantLock类
用法很简单,解决上面卖票问题的典型用法如下:
//创建一个锁对像,显式的对共享资源进行同步保护
public ReentrantLock myLock = new ReentrantLock();
myLock.lock(); //加锁
//critical section
System.out.println(Thread.currentThread().getName() + " ==== " + ticks --);
myLock.unlock();//去锁
critical section是临界区的意思,锁是可重入的,因为线程可以重复地获得已经持有的锁,锁保持一个持有计数器来跟踪对lock方法的嵌套调用,线程在每一次调用lock方法后都要调用unlock方法来释放锁。
三、线程间通信
这里将要说的线程间通信并不是传统意义上多线程/多任务之间采用管道,队列这些数据结构来进行数据传输,而仅是对共享资源并发访问的一种提高效率的机制。比如说,线程1(生产者producer)在生产完数据后,主动的去通知(notify)另一个线程共享资源可以访问了,然后线程2(消费者consumer)再进行读数据。
在这之前,先说一下线程的两种状态:阻塞和等待,它俩是不同的!
阻塞状态:当一个线程试图获取一个内部的对像锁,而该锁又被其它线程持有,则该线程进入阻塞状态(Blocking),当其它的线程释放该锁,并且线程调度器允许本线程持有它的时候,该线程变为非阻塞状态。
等待状态:线程等待另一个线程通知调度器一个条件时,它自已进入等待状态,在调用Object.wait()或Thread.join(),或是Lock和Condition(条件变量)这些时,就会出现这种情况。
阻塞强调的是,一个线程访问一个资源而不到,进行阻塞状态,直到能访问得到。
等待强调的是,一个线程主动的让自已空闲,等待一段时间过去,或是一个通知到来。
一个处于等待状态的线程在被notify后,并不会立即运行,因为notify仅仅是解除等待线程的等待状态,以便这些线程可以在当前线程退出同步方法后,再进行资源的访问。
再来说wait,notify,notifyAll,这三个方法是Object类的final方法,并且,这三个方法只能在同步方法中调用,因为每一个对像都有一个内部锁,这个锁又有一个内部的条件,而同步方法时synchronized的对像是this, 同步代码时synchronized的对像可以是任意的!wait方法是把当前线程添加到this对像的等待集中,notify是通知同一对像监视器中调用wait的线程,所以它们三个都只能在同步方法中调用。
总结一下:JAVA中每一个对像都有一个内部的锁和内部的条件,synchronized同步的是锁,而wait和notify是等待和通知条件变量。
典型案例——生产者,消费者问题
假如没有线程间通信机制(等待唤醒机制),那么情况如下图所示:
共享资源部分代码封装如下:
package Thread;
//共享数据对像
public class Student
{
private String name;
private int age;
private boolean flag;
//构造方法,flag == flase表示没有数据
public Student()
{
flag = false;
}
//生产数据
public synchronized void put(String name,int age)
{
//如果flag为真,则表明有数据,执行wait
//此时本线程被放入本对像的等待线程池中,并且自动释放锁
if(flag)
try
{
this.wait();
}catch (InterruptedException e)
{
e.printStackTrace();
}
//执行到此处表明没有数据,则生产数据
this.name = name;
this.age = age;
//生产完成,更改标志位,并通知wait线程
flag = true;
this.notify();
}
//消费数据
public synchronized void get()
{
//flag为真表明有数据,可以消费
if(flag == false)
{
try
{
this.wait();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
//程序执行到此,表明有数据,消费数据!
System.out.println(this.name + " --- " + this.age);
//消费完成,更改标志位并通知wait线程池中的第一个等待的线程
flag = false;
this.notify();
}
}
在实际应用中,线程Thread1只执行put来生产,线程Thread2只执行get来消费,如果不加入等待——通知这个机制,那么在Thread2和Thread1中分别会产生一个数据多次消费,数据未消费又生产覆盖的情况,因为线程的执行是随机的,不可控的。
关于synchronized,下面一段话来自一本书上的解说:
当线程执行synchronized时,检查传入的实参对像,并得到该对像的锁旗标。如果得不到,那么此线程会被加入到一个与该对像的锁旗标相关连的等待线程池中,一直等到该对像的锁旗标被归还,池中的等待线程就会得到该对像的锁旗标,然后继续执行下去。当线程执行完同步代码块时,就会自动释放它占有的同步对像的锁旗标。
最后,看一个图吧。