Java中的多线程-03

前言

前面说到了多线程之间同步的问题了。不过使用的例子确实几个线程输出数字,不是非常的形象。这里我们可以将情景改一下,改成火车站几个不同的窗口进行售票。如果不同的窗口售出同一张票或者说售出不存在的票问题就大了。其实代码和之前的还是一样的,只不过多加了一个情景罢了。

买票

经过情景的变化,我将代码也进行了修改。

public class Demo3 {
    public static void main(String[] args) {
        Client client = new Client();
        Thread thread1 = new Thread(client, "售票窗口一");
        Thread thread2 = new Thread(client, "售票窗口二");
        Thread thread3 = new Thread(client, "售票窗口三");

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

class Client implements Runnable {

    private int ticket = 100;

    @Override
    public void run() {
        while (ticket > 0) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ": " + ticket);
            ticket--;
        }
    }
}

其中上面的Thread.sleep是为了模拟网络的延迟,只一次我将网络的延迟降低了一点点。我们发现有很多的票没有卖出去,还有很多的票卖出去了好多张。关键是有点窗口还卖出去的负数的票。这个问题就很大了。

前面我们也说过了这是由于线程之间不同步导致的,我们需要采取一定的手段使得线程同步。最简单的方式就是将线程之间产生纠葛的地方转为单线程来执行。Java中提供了这样的一个工具。

线程同步机制

我们可以通过synchronized关键字,添加线程同步代码块。格式为

synchronized(同步监视器){
    ...// 需要同步的内容
}

通过上面的格式就可以构建出一个同步代码块,此时代码块中只能是单线程执行。不过这是如何实现的呢?那个同步监视器又是什么鬼?

别看同步监视器这个名字多么的高大上。其实任何的对象都可以作为同步监视器来使用。线程想要进入同步代码块的时候将取得同步监视器。此时外面的想要进入代码块的线程因为没有取得同步监视器所以会在代码块的外面进行等待。这也就是我们之前说的,等待锁同步的时候,线程会进入阻塞状态。这个锁就是通常意义上面的同步监视器。当线程离开同步代码块的时候将返回同步监视器。这样其他的线程就可以进入代码块。因为一次只能进入一个线程的缘故,同步代码块中的内容可以认为是单线程执行的。

不过需要注意的是,同步监视器必须要是多个线程唯一的。如果每个线程的锁都是一样的,那么就起不到同步线程的作用了。在上面的例子中,我们可以使用如下的方式。

class Client implements Runnable {

    private int mTicket = 100;
    private final Object mObject = new Object();

    @Override
    public synchronized void run() {
        synchronized (mObject) {
            while (mTicket > 0) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Thread.yield();
                System.out.println(Thread.currentThread().getName() + ": " + mTicket);
                mTicket--;
            }
        }
    }
}

这里使用的private final Object mObject就是一个同步监视器,而且是满足同步的需求的。所有的线程都是共用的这一个同步监视器。不过如果是使用继承Thread方式创建的线程要使用private static final Object mObject的方式,原理和上面的mTicket是相似的。

不过同步监视器要求是对象就可以。我们可以使用一个Dog类的对象作为同步监视器。不过有一个最方便的对象就是this,因为this对象也是唯一的。不过并不是所有的时候都可以使用this作为同步监视器的。比如使用继承Thread方式创建线程的话,每一个线程都有自己单独的一个this,此时就不可以把this作为同步监视器。不过,此时也有一个特别的对象可以作为同步监视器。在Java中,类其实也可以说是一个对象。每一个类都对应了一个Class对象,就是类名.class,这个对象是类唯一的,所以说是可以作为同步监视器的。关于这个类的具体的知识要等到之后谈到反射的时候才能够详细的说说了。

class Mythread extends Thread {

    Mythread(String name) {
        super(name);
    }

    Mythread() {
        super();
    }

    private static int num = 1;

    @Override
    public void run() {
        synchronized (Mythread.class) {
            while (num <= 100) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(getName()+": "+num);
                num += 2;
            }
        }
    }
}

上面就是使用反射对象作为同步监视器。

使用同步监视器虽然是解决了线程的同步问题,但是由于锁的存在,多线程之间会存在阻塞,所以被锁住的代码的执行效率甚至是还不然单线程的。所以说我们要尽量的减少同步代码块的大小。不过是要在保证数据的安全的情况下的。比如上Client类中,我们使用同步代码块将整个方法都包住了。那么可不可以少包那个while呢?比较那么地方也没有对数据进行操作。我稍微的实验了一下,结果是没有任何的问题的。但是分析一下。加入说现在还有最后的一张票。一个线程已经进入同步代码块,但是还没有进行num--的操作,此时另一个线程也进入了while,虽然进不了同步代码块,但是等到上面的线程走了,也会进入同步代码块中,所以说这样是不安全的。不过这个代码块也不是越大越好,太大了是完全没有多线程的效率的。我们要保证一个合适的位置。比如我们之前说过的单例模式。其中使用二次判断的方法,既安全也提高了多线程的效率。这个方法是多线程中经常会碰到的。

除了同步代码块之外,还可以使用同步方法。其实所谓的同步方法和同步代码块都是一样的。只要是方法内的所有的内容都是需要同步的话,我们就可以将这个方法声明为同步方法。

public synchronized void method(){
    ...// method content
}

public static synchronized void method{
    ...// method content
}

其实也就是类型之前加了一个synchronized关键词。不过给静态方法加锁和给非静态方法加锁还有点儿不一样。给非静态方法加锁相当于这样。

public void method(){
    synchronized(this){
        ...// methon content
    }
}

而给静态方法加锁相当于

public static void method(){
    synchronized(ClassName.class){
        ...// methond content
    }
}

一个加的是对象锁,一个加的是类锁,这点需要注意一下。

释放锁

线程取得锁之后什么时候会释放锁,这也是一个需要清楚的问题,正如我们需要明白线程的生命周期一样。

当线程在同步代码块或者方法中遇到了错误或异常,或者是已经执行完毕之后,会释放持有的锁。不过当线程在同步代码块中执行wait方法的时候也会释放持有的锁,当前线程会暂停,等待其他的线程调用notify方法来唤醒这个线程。这两个方法等到之后再说吧。

更多的情况是不会释放锁的。比如说调用sleep方法或者是调用yield方法,线程都只是暂停下来,但是不会将手中的锁释放。还有一点就是当其他的线程使用之前我们不推荐使用的方法suspend将线程挂起之后也是不会释放锁的,线程会暂停执行,知道调用了resume方法。

JDK5之后的锁

从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同 步锁对象来实现同步。同步锁使用Lock对象充当。

java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的 工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象 加锁,线程开始访问共享资源之前应先获得Lock对象

ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和 内存语义,在实现线程安全的控制中,比较常用的是``ReentrantLock`,可以 显式加锁、释放锁

使用的方式如下。

class A implements Runnable {

    private final ReentrantLock mLock = new ReentrantLock();
    private int num = 100;

    @Override
    public void run() {
        mLock.lock();
        while (num > 0) {
            System.out.println(num);
            num--;
        }
        mLock.unlock();
    }
}

这种方式相比于synchronized的就是要手动的开锁和关锁。使用这种方式的话,JVM会使用更少的时间来调度各个线程,所以说这种方式有更好的性能。

总结

上面简单的谈及了Java中线程同步的方式,还有线程同步中需要注意的问题。不过线程同步这一块还有一个比较重要的问题,那就是死锁的问题还没有说。


一枚小菜鸡