Java多线程(三)同步与锁定

转载请注明出处:http://blog.csdn.net/github_39430101/article/details/77488972
在并发编程中发生的最常见的一种情况是超过一个执行线程使用共享资源。

Java内存模型与多线程

Jvm有主内存(Main Memory)和工作内存,主内存其实就是我们平时说的Java堆内存,存放程序中所有的类实例、静态数据等变量,是多个线程共享的,而工作内存存放的是该线程从主内存中拷贝过来的变量以及访问方法所取得的局部变量,是每个线程私有的其他线程不能访问,每个线程对变量的操作都是以先从主内存将其拷贝到工作内存再对其进行操作的方式进行,多个线程之间不能直接互相传递数据通信,只能通过共享变量来进行。

这里写图片描述

同步

隐式锁synchronized,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该代码。synchronized修饰地方只有两个:

  • 一是在方法声明时使用,放在范围操作符(public等)之后,返回类型声明(void等)之前的方法名上面,代码如下:
public synchronized void synMethod(){ 
    .....
}
  • 二是修饰在代码块上面的,对某一代码块使用synchronized(Object),指定加锁对象:
public int synMethod(int a){
    synchronized(this){
        .....
    }
}

示例

public class Web12306 implements Runnable {
    private int num = 10;
    @Override
    public void run() {
        synchronized(this){  
           while(true){
            if(num<=0){
                break; //跳出循环
            }      
           System.out.println(Thread.currentThread().getName()+"抢到了"+num--); 
        }  
      }
  }

    public static void main(String[] args) {
        Web12306 web = new Web12306();
        Thread t1 = new Thread(web,"路人甲");
        Thread t2 = new Thread(web,"黄牛乙");
        Thread t3 = new Thread(web,"攻城狮");
        t1.start();
        t2.start();
        t3.start();
    }

}
//输出:
路人甲抢到了10
路人甲抢到了9
路人甲抢到了8
路人甲抢到了7
路人甲抢到了6
路人甲抢到了5
路人甲抢到了4
路人甲抢到了3
路人甲抢到了2
路人甲抢到了1

死锁

过多的同步容易造成死锁,这里我们举一个一手给钱一手给货的例子

public class Demo{
    public static void main(String[] args) {
        Object g = new Object();
        Object m = new Object();
        Test t1 = new Test(g,m);
        Test2 t2 = new Test2(g,m);
        Thread proxy = new Thread(t1);
        Thread proxy2 = new Thread(t2);
        proxy.start();
        proxy2.start();
    }
}
class Test implements Runnable{
    public Test(Object goods, Object money) {
        super();
        this.goods = goods;
        this.money = money;
    }
    Object goods;
    Object money;

    @Override
    public void run() {
        while(true){
            test();
        }
    }
    public void test(){
        synchronized(goods){
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized(money){

            }
        }
        System.out.println("一手给钱");
    }
}
class Test2 implements Runnable{
    Object goods ;
    public Test2(Object goods, Object money) {
        super();
        this.goods = goods;
        this.money = money;
    }
    Object money ;

    @Override
    public void run() {
        while(true){
            test();
        }
    }
    public void test(){
        synchronized(money){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized(goods){

            }
        }
        System.out.println("一手给货");
    }
}

生产者消费者模式(用来平衡生产者和消费者的处理能力)

生产者消费者问题是一个多线程同步问题的经典案例。该问题描述了两个共享固定大小缓冲区的线程–即所谓的生产者和消费者在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。于此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区慢时加入数据,消费者也不会在缓冲区中空时消耗数据。
要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,载唤醒消费者。通常常用的方法有信号灯法、管程等。如果解决方法不够完成,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。
这里写图片描述
信号灯法:
创建一个Movie类

public class Movie {
    private String pic;
    /*
     * 信号灯 flag -->true 生产者生产,消费者等待,生产完成后通知消费 flag -->false
     * 消费者消费,生产者等待,消费完成后通知生产
     */
    private boolean flag = true;

    public synchronized void play(String pic) {
        if (!flag) { // 等待
            try {
                this.wait();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        //开始生产
        try {
            Thread.sleep(500);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("生产了"+pic);
        //生产完毕
        this.pic = pic;
        //通知消费者
        this.notify();
        //生产者停下
        this.flag = false;
    }

    public synchronized void watch() {
        if (flag) {
            try {
                this.wait();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        //开始消费
        try {
            Thread.sleep(200);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("消费了"+pic);
        //消费完毕
        //通知生产
        this.notifyAll();
        this.flag = true;
    }
}

生产者电影工厂

public class Player implements Runnable {
    private Movie m;
    public Player(Movie m) {
        super();
        this.m = m;
    }
    @Override
    public void run() {
        for(int i=0;i<20;i++){
            if(0==i%2){
                m.play("飞跃疯人院");
            }else 
                m.play("肖申克的救赎");
        }
    }

}

消费者

public class Watcher implements Runnable {
    private Movie m;
    public Watcher(Movie m) {
        super();
        this.m = m;
    }
    @Override
    public void run() {
        for(int i=0;i<20;i++){
            m.watch();
        }
    }

}

主线程

public class App {
    public static void main(String[] args) {
        Movie m = new Movie();
        Player p = new Player(m);
        Watcher w = new Watcher(m);
        new Thread(p).start();
        new Thread(w).start();
    }
}

运行结果:
这里写图片描述

任务调度

  • Timer定时器类
  • TimerTask任务类
  • 通过java timer timertask:(Spring的任务调度就是通过他们来实现的)
  • 在这种实现方式中,Timer类实现的是类似闹钟的功能,也就是定时或者每隔一定时间触发一次线程。其实,Timer类本身实现的就是一个线程,只是这个线程是用来实现调用其他线程的。而TimerTask类是一个抽象类,该类实现了Runnable接口,所以按照前面的介绍,该类具备多线程的能力
  • 在这种实现方式中,通过继承TimerTask使该类获得多线程的能力,将需要多线程执行的代码书写在run方法内部,然后通过Timer类启动线程的执行。
  • 在实际使用时,一个Timer可以启动任意多个TimerTask实现的线程,但是多个线程之间会存在阻塞。所以如果多个线程之间如果需要完全独立运行的话,最好还是一个Timer启动一个TimerTask实现。
public class TimeDemo {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask(){
            @Override
            public void run(){
                System.out.println("so easy....");
            }
        }, new Date(System.currentTimeMillis()+1000), 200);
    }
}

运行结果:
这里写图片描述

总结

1、sleep()

使当前线程(即调用该方法的线程)暂停执行一段时间,让其他线程有机会继续执行,但它并不释放对象锁。也就是说如果有synchronized同步快,其他线程仍然不能访问共享数据。注意该方法要捕捉异常。

2、join()

join()方法使调用该方法的线程在此之前执行完毕,也就是等待该方法的线程执行完毕后再往下继续执行。注意该方法也需要捕捉异常。

3、yield()

该方法与sleep()类似,只是不能由用户指定暂停多长时间,并且yield()方法只能让同优先级的线程有执行的机会。yield方法使当前线程让出CPU占有权,但让出的时间是不可设定的。不会释放锁标志。

4、wait()和notify()、notifyAll()

这三个方法用于协调多个线程对共享数据的存取,所以必须在synchronized语句块内使用。synchronized关键字用于保护共享数据,阻止其他线程对共享数据的存取,但是这样程序的流程就很不灵活了,如何才能在当前线程还没退出synchronized数据块时让其他线程也有机会访问共享数据呢?此时就用这三个方法来灵活控制。

wait()方法使当前线程暂停执行并释放对象锁标示,让其他线程可以进入synchronized数据块,当前线程被放入对象等待池中。当调用notify()方法后,将从对象的等待池中移走一个任意的线程并放到锁标志等待池中,只有锁标志等待池中线程能够获取锁标志;如果锁标志等待池中没有线程,则notify()不起作用。
notifyAll()则从对象等待池中移走所有等待那个对象的线程并放到锁标志等待池中。

5、关键字synchronized
该关键字用于保护共享数据,当然前提条件是要分清哪些数据是共享数据。每个对象都有一个锁标志,当一个线程访问到该对象,被Synchronized修饰的数据将被”上锁”,阻止其他线程访问。当前线程访问完这部分数据后释放锁标志,其他线程就可以访问了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章