1.多个线程在操作共享的数据
2. 操作共享数据的线程代码有多条
用示例解释如下:开启了四条线程 ,同时操作共享的数据,并且有两条操作共享数据的线程代码
class Ticket implements Runnable//extends Thread
{
private int tick = 100;
public void run()
{
while(true)
{
if(tick>0) //操作一次共享数据
{
System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);//第二次操作共享数据
}
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t);//创建了一个线程;
Thread t2 = new Thread(t);//创建了一个线程;
Thread t3 = new Thread(t);//创建了一个线程;
Thread t4 = new Thread(t);//创建了一个线程;
t1.start();
t2.start();
t3.start();
t4.start();
}//运行过程中票出现了0 -1 -2情况。这与实际已经不符合了
}
解决思路:
就是将多条操作共享数据的线程代码进行封装,当有线程在执行这些代码的时候,其他线程不能参与运算。必须是当前线程执行完 这些代码后,其它线程才能参与运算。 通常使用同步代码块或者 同步函数 。 synchronized
同步的出现,解决了线程的安全问题。却相对降低了效率,因为同步外的线程都会去判断同步锁。
同步的前提:必须有多个线程并且使用同一个锁。
仍旧拿上个例子来说,在Ticket中定义一个Object类型的成员变量obj作为锁,把while 循环中的 if语句放入同步代码块中就解决了出现0 -1 -2的情况。
同步函数和同步代码块的区别:
同步函数的锁是固定的this(调用run方法的对象)
同步代码块的锁是任意的对象 。建议使用同步代码块
静态同步函数使用的锁是该函数所属字节码文件对象,可以用getClass方法获取,也可以用当前类名.class表示。·
如果不明白单例模式,请看这篇文章 java设计模式
class Single
{ // 饿汉式
private static final Single s = new Single();
private Single(){}
public static Single getInstance()
{
return s;// 只有一条操作共享数据的代码
}
}
//懒汉式
class Single
{
private static Single s = null;
private Single(){}
public static Single getInstance()
{
//if(s==null)
//{
synchronized(Single.class)
{
if(s==null)
//--->A;
s = new Single();
}
//}
return s;
}
}
请看上面的代码,对于饿汉式来说,仅仅有一条操作共享数据的代码,所以不会出现线程安全问题,也能保证对象的单一。
但对于懒汉式来说,情况就不同了。请看上面懒汉式的单例代码,当不加同步代码时,某个时刻会存在这样的情况(以两个线程为例): 线程A判断s=null成立,进入if语句,此时恰好时间片切换到线程B(线程A还没有创建单例对象),于是线程B开始执行,也判断了s是否为空,为真,于是也进入了if语句,并创建了对象,此时时间片又切换到线程A,因为已经进入了if语句,就又创建了一次单例对象,这样,就不符合我们的单例设计模式了。 我们加入同步之后,当线程A挂起,线程B开始执行后,由于没有锁,无法执行,只能乖乖滴等线程A执行完毕,释放了锁,B才能执行,此时,已经创建了对象,s!= null ,不会再创建对象了,就保证了对象的单一 。 如上面的代码所示。又在同步前面加了一层if判定是为了尽量避免别的线程去判断锁,判断锁太消耗资源,稍稍提高下效率,也可以注释掉。
------------------
在了解死锁前我们有必要了解一下死锁是怎么产生的呢?
死锁是两个及以上线程在执行过程中,由于抢夺资源而造成的互相等待对方的资源,在没有得到对方资源的情况下,不会释放自身持有的资源,这种情况就叫做死锁。
死锁的常见的形式之一: 同步的嵌套
class Ticket implements Runnable
{
private int tick = 1000;
Object obj = new Object();
boolean flag = true;
public void run()
{
if(flag)
{
while(true)
{
synchronized(obj)
{
show();
}
}
}
else
while(true)
show();
}
public synchronized void show()//this
{
synchronized(obj)
{
if(tick>0)
{
try{Thread.sleep(10);}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"....code : "+ tick--);
}
}
}
}
class DeadLockDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
try{Thread.sleep(10);}catch(Exception e){} //使主线程睡眠,不继续进行
t.flag = false;
t2.start();
}
}
如上述代码所示,当主线程运行起来之后,就把t1线程启动了,此时就去执行run里面的方法,t1线程拿到obj和this锁,开始执行代码,但是可能存在这样的一个时刻,当t1线程执行完show方法,释放了this锁之后,时间片切换到主线程了,主线程就将flag改为false ,并且启动t2线程。此时t2线程开始运行,由于flag为false,t2线程执行run方法中的else中的语句,执行同步函数show() ,t2持有this锁,进入发现没有obj锁,无法继续执行,因为是while不断循环,t1拿着obj锁进入同步代码块发现并没有this锁,不能执行同步函数,于是两个线程都在等对方释放锁,程序就宕起了。这就是死锁。
多个线程在处理同一个个资源,但是任务却不同。我们要注意的是在操作共享的数据的时候,注意保持线程同步,避免出现错误的结果。
涉及的方法:
1.wait() 使线程处于冻结状态,释放cpu执行权和执行资格(释放锁),被wait的线程会被存储到线程池(即容器,用于存放没有消亡的线程)中。
2. notify() 唤醒线程池中的一个线程(任意),注意:唤醒的是同一个锁上的任意一个线程
3. notifyAll()唤醒线程池中的所有线程。即是所有线程获得执行资格。
注意: 这些方法都必须定义在同步中,因为这些方法是用于操作线程状态的方法,必须要明确到底操作的是哪个锁上的线程。 不同的锁存在着 不同的线程池,用锁区分。例如,r.wait() 之后就用r.notify()唤醒r线程池中的线程。
查看api发现,这些方法都定义在了 Object类中, 这是由于这些方法是监视器的方法。监视器其实就是锁,锁可以是任意对象,任意的对象调用方式一定定义在Object类中。
先写一个简单的需求: 使用两个线程,一个生产烤鸭,一个消费烤鸭 。简单代码如下:
class Resource{
private String name;
private int count = 1 ;
private boolean flag=false;
public synchronized void set(String name){
if(flag)
try{this.wait();}catch(Exception x){}
this.name =name+count;
count++ ;
System.out.println(Thread.currentThread().getName()+"..生产者"+this.name);
flag = true ; notify();
}
public synchronized void out(){
if(!flag)
try{this.wait();}catch(Exception x){}
System.out.println(Thread.currentThread().getName()+"..消费者......."+this.name);
flag = false ; notify() ;
}
}
public class ProduceDemo{
public static void main(String[] args) {
Resource r = new Resource();
Producer pro = new Producer(r);
Consumer con =new Consumer(r);
Thread t0 = new Thread(pro);
//Thread t1 = new Thread(pro);// 暴露多生产多消费问题时,打开此注释
Thread t2 =new Thread(con) ;
//Thread t3 =new Thread(con) ;
t0.start(); t2.start() ;
//t1.start() ;t3.start();
}
}
class Consumer implements Runnable{
Resource r ;
Consumer(Resource r){
this.r=r;
}
public void run(){
while(true)
r.out();
}
}
class Producer implements Runnable{
private Resource r;
Producer(Resource r){
this.r=r;
}
public void run(){
while(true)
r.set("烤鸭..");
}
}
这里我们使用的是同步函数,保证生产过程结束,消费者才去消费 。但是问题来了,假如我们的小作坊要扩展业务(多个生产,多个消费),此时把上面代码中的注释解开,运行代码就会有以下的情况发生:
Thread-1..生产者烤鸭..2718
Thread-0..生产者烤鸭..2719
Thread-3..消费者.......烤鸭..2719
Thread-2..消费者.......烤鸭..2719
生产者生产了两个烤鸭,但是一只没有被消费,另一只被消费了两次,这是什么原因呢? 回到代码中分析如下:
在某个时间点上,存在着这样的情况,线程t1判断标记为真,已经wait(),而且之前t0也已经wait() (注意这完全是有可能的),此时消费者刚消费完一只烤鸭,将flag改为false,并唤醒this锁的线程池中的一个线程,假如唤醒了t1,t1继续执行下面的代码(并没有返回去判断if中的标记),就生产了烤鸭2178 ,并把flag标记改为true,又唤醒一个线程,不幸运的是t0被唤醒了,于是又生产了一只烤鸭,到这里已经出现线程安全问题了,生产了两只。同理,对消费者来说也是一样,由于唤醒线程的不确定性使得,一只烤鸭被消费了两次 。分析问题产生的原因,是由于挂起的线程被唤醒后,并没有再去判断一次标记,才导致这种情况发生。
怎么样让它返回去在判断一次标记呢? 使用while ,我们知道if语句仅仅执行一次,但是while由于是个循环结构,会在返回去判断一次。到这里问题似乎已经解决了,可是情况是这样吗? 我们将set和out中的if语句改为while,并执行代码,却发现了程序宕机 ,也就是死锁。这是由于存在这样的片段,消费者线程t2抢到了执行权,消费了一直烤鸭后将标记改为false,并唤醒了一个线程,结果就唤醒了另一个消费者线程t3,于是t3就进入了等待状态,接着t2又抢到了执行权,也进入了等待状态。此时两个生产者线程中假如t0抢到执行权,就生产了一只烤鸭,把标记改为true ,问题就出现在这里,如果唤醒的仍旧是生产者中的一个线程,由于标记为true,就进入了等待状态,此时线程池存活状态的就只有生产者中的一个线程,一执行立即也进入了等待状态,线程池中的四个线程全部进入等待状态,程序就宕机了。
分析原因,其实就是唤醒任意一个线程造成的,如果消费者线程唤醒的是生产者中的一个线程,那么程序就和谐了,所以如何保证生产者 唤醒消费者中的一个线程,是解决问题的关键。可以使用notifyAll()方法,将线程池中的所有线程都唤醒,这就把问题解决了。
总结如下:
至此附上已经解决问题的代码。
class Resource{
private String name;
private int count = 1 ;
private boolean flag=false;
public synchronized void set(String name){
while(flag)
try{this.wait();}catch(Exception x){}
this.name =name+count;
count++ ;
System.out.println(Thread.currentThread().getName()+"..生产者"+this.name);
flag = true ; notifyAll();
}
public synchronized void out(){
while(!flag)
try{this.wait();}catch(Exception x){}
System.out.println(Thread.currentThread().getName()+"..消费者......."+this.name);
flag = false ; notifyAll() ;
}
}
public class ProduceDemo{
public static void main(String[] args) {
Resource r = new Resource();
Producer pro = new Producer(r);
Consumer con =new Consumer(r);
Thread t0 = new Thread(pro);
Thread t1 = new Thread(pro);// 暴露多生产多消费问题时,打开此注释
Thread t2 =new Thread(con) ;
Thread t3 =new Thread(con) ;
t0.start(); t2.start() ;
t1.start() ;t3.start();
}
}
class Consumer implements Runnable{
Resource r ;
Consumer(Resource r){
this.r=r;
}
public void run(){
while(true)
r.out();
}
}
class Producer implements Runnable{
private Resource r;
Producer(Resource r){
this.r=r;
}
public void run(){
while(true)
r.set("烤鸭..");
}
}
但是notifyALl()虽然解决了问题,但是己方线程也被唤醒了,还要再去判断标记,浪费资源,降低了性能。怎么样才能仅仅唤醒对方的线程呢? 有! jdk1.5以后,将同步和锁封装成了对象 。并将操作锁的隐式方式定义到了该对象中,将隐式动作变成了显式动作。 java.util.concurrent.locks 包下提供的Lock 接口。请看Lock的源码。四个方法。lock和unlock方法用来获取锁和释放锁。newCondittion()方法,返回一个Condition对象。其一个常用实现子类ReentrantLock
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();// 返回绑定到此Lock实例的新Condition实例
}
Condition 接口,位于 java.util.concurrent.locks 包下,将Object监视器方法(wait,notify,notifyAll)分解为不同的对象,使得可以通过这些对象实现与任意Lock组合使用。Lock替代了synchronized方法和语句的使用,Condition替代了Object监视器方法的使用。
常用方法: void await() 造成当前线程在接到信息或被中断之前一直处于等待状态
boolean await(long time,TimeUnit unit)造成当前线程在接到信号、被中断或到达等待时间之前一直处于等待状态
void signal() 唤醒一个等待线程 。
void signalAll() 唤醒所有等待线程。
到此就可以允许一个锁上有多组监视器,仍旧拿生产者消费者问题来说,我们可以在生产者中使用消费者监视器唤醒消费者线程。这样就避免了原本的会唤醒己方锁,造成资源浪费的情况。
import java.util.concurrent.locks.Lock ;
import java.util.concurrent.locks.Condition ;
import java.util.concurrent.locks.ReentrantLock;
class Resource{
private String name;
private int count = 1 ;
private boolean flag=false;
//创建一个锁对象。
Lock lock = new ReentrantLock();
// 通过已有的锁获取该锁上的监视器对象。
Condition pro =lock.newCondition();
Condition cus =lock.newCondition();
public void set(String name){
lock.lock();
try{
while(flag)
try{pro.await();}catch(Exception x){}
this.name =name+count;
count++ ;
System.out.println(Thread.currentThread().getName()+"..生产者"+this.name);
flag = true ;
cus.signal(); //使用消费者锁的监视器唤醒消费者中的一个线程。
}finally{
lock.unlock();
}
}
public void out(){
lock.lock() ;
try{
while(!flag)
try{cus.await();}catch(Exception x){}
System.out.println(Thread.currentThread().getName()+"..消费者......."+this.name);
flag = false ;
pro.signal(); // 唤醒生产者中的一个线程。
}finally{
lock.unlock();
}
}
}
public class ProduceDemo{
public static void main(String[] args) {
Resource r = new Resource();
Producer pro = new Producer(r);
Consumer con =new Consumer(r);
Thread t0 = new Thread(pro);
Thread t1 = new Thread(pro);// 暴露多生产多消费时,打开此注释
Thread t2 =new Thread(con) ;
Thread t3 =new Thread(con) ;
t0.start(); t2.start() ;
t1.start() ;t3.start();
}
}
class Consumer implements Runnable{
Resource r ;
Consumer(Resource r){
this.r=r;
}
public void run(){
while(true)
r.out();
}
}
class Producer implements Runnable{
private Resource r;
Producer(Resource r){
this.r=r;
}
public void run(){
while(true)
r.set("烤鸭..");
}
}
1. wait可以指定时间也可以不指定。sleep必须指定时间
2,在同步中时,对cpu的执行权和锁的处理不同
wait :释放执行权,释放锁
sleep: 释放执行权,不释放锁。
同步中只有一个线程可以运行,但是可以存在多个活着的线程。借用下面的简单代码帮助理解:
class Demo{
void show(){
synchronized(this){ // t0 t1 t2
wait();
}
}
void method(){
synchronized(this){
notifyAll() ;
}
}
}
某个片段,t0,t1,t2执行show方法,结果就都wait了,此时假如t3执行了method方法,由于使用的是同一个锁,所以t0,t1,t2全部被唤醒了。这就证明了同步中可以存在多个活着的线程,但是存活(具有执行权)并不代表可以执行,在t0 ,t1,t2中只有拿到锁的线程才能继续执行下面的代码,并且当执行完同步代码块,释放锁之后其余线程才可能获取到锁,执行代码。