【十八掌●基本功篇】第一掌:Java之多线程--2-join、同步、死锁、等待

这一篇博文是【大数据技术●降龙十八掌】系列文章的其中一篇,点击查看目录:这里写图片描述大数据技术●降龙十八掌


系列文章:
【十八掌●基本功篇】第一掌:Java之IO
【十八掌●基本功篇】第一掌:Java之多线程–1-一些概念
【十八掌●基本功篇】第一掌:Java之多线程–2-join、同步、死锁、等待

1、join() 方法

join()方法可以理解为线程插队。停止当前线程,先执行插入的线程,当插入的线程执行完毕后,再执行当前线程。看下面的例子:


package join;

/**
 * Created by 鸣宇淳 on 2017/12/7.
 */
public class MyJoinRunner implements Runnable {
    //子线程
    public void run() {
        for (int n = 0; n < 100; n++) {
            System.out.println(Thread.currentThread().getName() + ":" + n);
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class MyJoin {
    public static void main(String[] args) throws InterruptedException {

        MyJoinRunner runner=new MyJoinRunner();

        Thread t1=new Thread(runner,"子线程");
        t1.start();

        //将子线程插队,先执行子线程,然后再执行其他线程(主线程)
        t1.join();

        for (int n = 0; n < 100; n++) {
            System.out.println(Thread.currentThread().getName() + ":" + n);
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

输出结果:

子线程:0
子线程:1
子线程:2
子线程:3
子线程:4
子线程:5
子线程:6
.......
.......

main:0
main:1
main:2
main:3
main:4
main:5
.......
.......

2、一个多线程会出问题的例子

假设有个场景是自动卖票,一共有5张票,新建三个线程来同时卖票,往往就会出问题。如下实例所示。


/**
 * Created by 鸣宇淳 on 2017/12/8.
 */
public class SellTicketRunner implements Runnable {

    private int ticket = 5; //一共有n张票

    //一个子线程执行的方法
    public void run() {
        while (true) {
            if (this.ticket > 0) {
                //判断如果票大于0,就先睡眠,以达到模拟的效果。
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //卖票
                this.ticket--;
                //打印剩余票数
                System.out.println(Thread.currentThread().getName() + "正在卖票;剩余=" + this.ticket);
            } else {
                break;
            }
        }
    }
}

public class MySellTicker {
    public static void main(String[] args) throws InterruptedException {
        SellTicketRunner runner=new SellTicketRunner();
        System.out.println("卖票开始.....");

        Thread t1=new Thread(runner,"线程一");
        Thread t2=new Thread(runner,"线程二");
        Thread t3=new Thread(runner,"线程三");

        t1.start();
        t2.start();
        t3.start();        

        System.out.println("买票结束!");
    }
}

输出为:

线程一正在卖票;剩余=4
线程二正在卖票;剩余=2
线程三正在卖票;剩余=2
线程一正在卖票;剩余=1
线程三正在卖票;剩余=-1
线程二正在卖票;剩余=-1
线程一正在卖票;剩余=-2
买票结束!

会发现,出现票超卖的情况,这是因为在判断余票数量后、卖票操作前的阶段,有可能有多个线程进入,然进行卖票操作。这就需要我们使用锁和同步进行限制。

3、同步和锁定

(1) 对象锁

java中每个对象都有一个内置锁。可以使用任意一个对象上的锁,来实现线程的同步。看下面的实例,是将上面有问题的卖票代码,添加上对象锁,来控制线程同步,以解决超卖的问题。

package sellticket;

/**
 * Created by 鸣宇淳 on 2017/12/8.
 */
public class SellTicketRunner implements Runnable {

    private int ticket = 5; //一共有n张票

    private String lock=""; //创建一个对象,使用这个对象上的锁来实现线程同步

    //一个子线程执行的方法
    public void run() {
        while (true) {
            synchronized (lock) {
                //将需要保护的代码块,锁住,防止多个线程同时进入这个代码块
                if (this.ticket > 0) {
                    //判断如果票大于0,就先睡眠,以达到模拟的效果。
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //卖票
                    this.ticket--;
                    //打印剩余票数
                    System.out.println(Thread.currentThread().getName() + "正在卖票;剩余=" + this.ticket);
                } else {
                    break;
                }
            }
        }
    }
}

public class MySellTicker {
    public static void main(String[] args) throws InterruptedException {
        SellTicketRunner runner=new SellTicketRunner();

        Thread t1=new Thread(runner,"线程一");
        Thread t2=new Thread(runner,"线程二");
        Thread t3=new Thread(runner,"线程三");

        t1.start();
        t2.start();
        t3.start();

        t1.join();
        t2.join();
        t3.join();

        System.out.println("买票结束!");
    }
}

(2) 方法锁

可以在方法上添加上一个synchronized关键字,表明这个一个同步方法,通过锁定一个方法,同时只让一个线程执行这个方法,


package sellticket;

/**
 * Created by 鸣宇淳 on 2017/12/8.
 */
public class SellTicketRunner2 implements Runnable {
    private int ticket = 5; //一共有n张票

    public void run() {
        while (true) {
            boolean isQuit = sell();
            if (!isQuit) {
                break;
            }
        }
    }

    //在方法上添加一个synchronized关键字,表名是同步方法
    //将需要保护的方法,锁住,防止多个线程同时进入这个方法
    private synchronized boolean sell() {
        System.out.println(Thread.currentThread().getName() + "进入");

        boolean isHav;
        if (this.ticket > 0) {
            isHav = true;
            //判断如果票大于0,就先睡眠,以达到模拟的效果。
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //卖票
            this.ticket--;
            //打印剩余票数
            System.out.println(Thread.currentThread().getName() + "正在卖票;剩余=" + this.ticket);

        } else {
            isHav = false;
        }
        System.out.println(Thread.currentThread().getName() + "退出\n");
        return isHav;
    }
}


public class MySellTicker {
    public static void main(String[] args) throws InterruptedException {
        SellTicketRunner2 runner=new SellTicketRunner2();

        System.out.println("卖票开始.....");

        Thread t1=new Thread(runner,"线程一");
        Thread t2=new Thread(runner,"线程二");
        Thread t3=new Thread(runner,"线程三");

        t1.start();
        t2.start();
        t3.start();
    }
}

当sell方法不加synchronized关键字时输出,看上去售票过程比较乱:


卖票开始.....
线程二进入
线程一进入
线程三进入
线程三正在卖票;剩余=3
线程一正在卖票;剩余=3
线程二正在卖票;剩余=3
线程一退出

线程一进入
线程三退出

线程二退出

线程二进入
线程三进入
线程二正在卖票;剩余=2
线程二退出

线程二进入
线程三正在卖票;剩余=2
线程三退出

线程三进入
线程一正在卖票;剩余=2
线程一退出

线程一进入
线程一正在卖票;剩余=1
线程一退出

线程一进入
线程一退出

线程三正在卖票;剩余=-1
线程三退出

线程三进入
线程三退出

线程二正在卖票;剩余=-1
线程二退出

线程二进入
线程二退出

如果加上synchronized,就能保证同时只有一个线程在执行sell方法,输出为:

卖票开始.....
线程一进入
线程一正在卖票;剩余=4
线程一退出

线程一进入
线程一正在卖票;剩余=3
线程一退出

线程一进入
线程一正在卖票;剩余=2
线程一退出

线程一进入
线程一正在卖票;剩余=1
线程一退出

线程一进入
线程一正在卖票;剩余=0
线程一退出

线程一进入
线程一退出

线程二进入
线程二退出

线程三进入
线程三退出

(3) static 方法的同步

在非static方法上添加synchronized,相当于对当前类的对象this加锁。

    private synchronized void fun() throws InterruptedException {       
        Thread.sleep(100);
    }

等同于:

    private void fun() throws InterruptedException {
        synchronized (this) {
            Thread.sleep(100);
        }
    }

但是在static方法是先于类的对象而存在的,当没有实例化对象的时候,static方法已经可以调用了,那么在static方法上添加synchronized ,是对什么加锁呢?其实是对类的class对象加锁。

public class MyStaticDemo {
    private synchronized static void fun() throws InterruptedException {
        Thread.sleep(100);
    }
}

等同于:

public class MyStaticDemo {
    private static void fun() throws InterruptedException {
        synchronized (MyStaticDemo.class) {
            Thread.sleep(100);
        }
    }
}

4.死锁

一个死锁的实例:


/**
 * Created by 鸣宇淳 on 2017/12/11.
 */
public class DeadLockRunner implements Runnable {

    String[] source;
    String[] target;

    public DeadLockRunner(String[] source,String[] target)
    {
        this.source=source;
        this.target=target;
    }

    public void run() {
        synchronized (source) {
            System.out.println(Thread.currentThread().getName() + ":进入source到target拷贝");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            synchronized (target) {
                for (int n = 0; n < source.length; n++) {
                    target[n] = source[0];
                }
            }

            System.out.println(Thread.currentThread().getName() + ":完成source到target拷贝");
        }
    }
}
public class DeadLock {

    public static void main(String[] args) {

        String[] source = new String[]{"a", "b", "c", "d"};
        String[] target = new String[]{"1", "2", "3", "4"};

        DeadLockRunner runner1 = new DeadLockRunner(source,target);
        DeadLockRunner runner2 = new DeadLockRunner(target,source);

        Thread t1=new Thread(runner1,"线程1");
        Thread t2=new Thread(runner2,"线程2");

        t1.start();
        t2.start();
    }
}

运行时可以发现输出:

线程2:进入source到target拷贝
线程1:进入source到target拷贝

然后就卡在这里不继续运行了,根据输出结果和分析代码可得知,线程2是先执行的,线程2先锁定source后,就sleep了,然后线程1进入后锁定了target,也sleep了,当线程2醒来后要求target,但是被线程1锁定了,线程1醒来要求source,但是被线程2锁定了,两个线程就造成了死锁。

避免死锁的方法:
要确定获得锁的顺序,然后整个程序要遵守该顺序,按相反的顺序释放锁。

5. 线程等待

必须在同步环境内调用wait()、notify()、notfiyAll()方法,也就是说在在同步代码块或者同步方法内才能进行等待或者唤醒。wait()、notify()、notfiyAll()方法是Object的实例方法,所以每个对象上都可以有一个线程列表。

实例1:

package wait;

/**
 * Created by 鸣宇淳 on 2017/12/11.
 */
public class MyWaitRunner implements Runnable {

    private int total;

    public MyWaitRunner(int n) {
        this.total = n;
    }

    public void run() {
        System.out.println(Thread.currentThread().getName() + "进入执行");
        synchronized (this) {
            for (int i = 0; i < total; i++) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //唤醒对象监视器上的单个线程,当前是唤醒线程1
                notify();
            }
        }
        System.out.println(Thread.currentThread().getName() + "执行完毕");
    }
}

public class MyWait {

    public static void main(String[] args) throws InterruptedException {

        MyWaitRunner runner1=new MyWaitRunner(100);
        Thread t1=new Thread(runner1,"线程1");
        t1.start();

        synchronized (t1)
        {
            System.out.println("主线程做一些事情......");
            System.out.println("等待子线程完成");
            //等待线程1完成
            t1.wait();
            System.out.println("子线程完成");
        }
    }
}

实例2:

下面这个例子是模拟一个场景,司机开车带着乘客去北京游览故宫,乘客线程上车后,通知司机开车,然后乘客睡觉,让司机在到达后叫醒自己,通过这个例子看一下两个线程同步和通知功能的使用方法。



/**
 * Created by 鸣宇淳 on 2017/12/12.
 */
public class Passenger implements Runnable {

    //乘客线程方法
    public void run() {
        synchronized (ToBeiJing.lock) {
            System.out.println("[乘客]已经上车");
            try {
                //通知司机开车
                System.out.println("[乘客]通知司机开车");
                ToBeiJing.driverThread.start();

                System.out.println("[乘客]开始睡觉,到北京叫我");
                ToBeiJing.lock.wait();

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("[乘客]醒了,开始游览故宫");
        }
    }
}

public class Driver implements Runnable {

    //司机线程方法
    public void run() {

        for (int n = 0; n < 5; n++) {
            System.out.println("[司机]正在开车");
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("[司机]已经到北京了");

        synchronized (ToBeiJing.lock) {
            System.out.println("[司机]叫醒乘客");
            //叫醒乘客
            lock.notify();
        }
    }
}

public class ToBeiJing {

    //定义一个锁,线程依据这个锁实现同步
    public static String lock = "";
    //司机线程
    public static Thread driverThread;
    //乘客线程
    public static Thread passengerThread;

    public static void main(String[] args) throws InterruptedException {

        Driver driver = new Driver();
        Passenger passenger = new Passenger();

        driverThread = new Thread(driver, "司机线程");
        passengerThread = new Thread(passenger, "乘客线程");

        //乘客线程启动
        passengerThread.start();
    }
}

第三个实例:

这是个经典的生产者消费者例子,有多个生产者和多个消费者同时在生产和消费,有一个仓库,最大存储量是固定的,所以在生产和消费的时候要收到仓库量的限制,就需要到线程的同步和锁。看以下代码。


import java.util.ArrayList;
import java.util.List;

/**
 * Created by 鸣宇淳 on 2017/12/12.
 */
public class Demo {

    public static void main(String[] args) {

        //各个线程中,要生产或者消费的个数
        Integer[] producerNum = new Integer[]{10, 20, 30, 25, 40, 60};
        Integer[] consumerNum = new Integer[]{20, 25, 35, 15, 50};

        Godown godown = new Godown();
        List<Thread> threads = new ArrayList<Thread>();

        //创建生产者线程
        for (Integer p : producerNum) {
            Thread t = new Thread(new Producer(p, godown));
            threads.add(t);
        }

        //创建消费者线程
        for (Integer c : consumerNum) {
            Thread t = new Thread(new Consumer(c, godown));
            threads.add(t);
        }

        System.out.println("当前仓库中数量:" + godown.currNum);

        //启动生产者和消费者线程
        for (Thread t : threads) {
            t.start();
        }
    }
}


//仓库类
public class Godown {

    //最大库存量
    public static final int MAX_SIZE = 100;

    public int currNum;//当前库存量

    /*
    生产方法
     */
    public synchronized void produce(int addNum) throws InterruptedException {
        while (addNum + currNum > MAX_SIZE) {
            System.out.println("要生产" + addNum + ",仓库空位数为" + (MAX_SIZE - currNum) + ",暂停生产");
            this.wait();
        }

        //可以生产
        currNum = currNum + addNum;
        System.out.println("生产了" + addNum + ",当前库存量为:" + currNum);
        //唤醒
        this.notifyAll();
    }

    public synchronized void consume(int needNum) throws InterruptedException {
        while (currNum < needNum) {
            System.out.println("要求消费" + needNum + "个,剩余" + currNum + "个,不能消费,等待生产");
            this.wait();
        }
        currNum = currNum - needNum;
        System.out.println("消费了" + needNum + "个,当前库存量为:" + currNum);

        //唤醒
        notifyAll();
    }
}

/**
 * Created by 鸣宇淳 on 2017/12/12.
 * <p>
 * 生产者类
 */
public class Producer implements Runnable {

    private int addNum;
    private Godown godown;

    public Producer(int addNum, Godown godown) {
        this.addNum = addNum;
        this.godown = godown;
    }

    public void run() {
        try {
            this.godown.produce(this.addNum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

/**
 * Created by 鸣宇淳 on 2017/12/12.
 * 消费者
 */
public class Consumer implements Runnable {
    private int needNum;
    private Godown godown;

    public Consumer(int needNum, Godown godown) {
        this.needNum = needNum;
        this.godown = godown;
    }

    public void run() {
        try {
            this.godown.consume(needNum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
发布了74 篇原创文章 · 获赞 74 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/chybin500/article/details/78783321