多线程初阶(2)

说到多线程编程,一定少不了线程安全这个话题。我们前面了解了线程的原理以及线程与进程的关系。线程之间共享资源,这就代表了在多线程编程中一定会产生冲突,所以我们需要在敲代码时保证线程安全,避免这样的问题发生。

我们先看一个代码案例

public class Test10 {
    static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });

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

        //保证线程都执行完
        Thread.sleep(1000);

        System.out.println(count);
    }
}

在我们看来,运行的结果应该是100000,但是事实并非如此。

运行结果:

1.随机调度

count++;

这段代码是看似是一个指令,但实际上是分步执行的。

分为三步的:

1.读取数据

2.修改数据

3.放回内存

在执行过程中,CPU资源是随时会被调度走的,也就是说,如果执行到了读取内存,有可能会被立刻调度走的。这就是所谓的随机调度。

为了解释上面的案例可以画一个时间线:

这也就是造成上面现象的原因之一。

那我们应该如何解决呢?

解决方案1:

利用join方法,在t1执行完后再执行t2,这样虽然能解决问题,但是失去了并发执行的意义,串行执行的效率也比较低。

public class Test10 {
    static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });

        t1.start();
        t1.join();
        t2.start();
        t2.join();

        //保证线程都执行完
        Thread.sleep(1000);

        System.out.println(count);
    }
}

2.锁(synchronized)

对于解决上述的线程问题,引入了锁这一概念。

锁是什么,我们可以举个具体的例子来了解一下:

我们可以把锁看成家里的锁,当家里没人时,门是从外面锁上的,这时拥有钥匙的人就可以打开锁并进去执行任务,但为了在执行任务的时候是安全的,所以会从里面锁上,这时外面的人就算有钥匙也打不开门,当里面的人执行完任务的出来后还会把门带上,这时外面拥有钥匙的人就可以开门进去了。这其实就是锁是如何解决线程安全问题的类似原理。

public class Test10 {
    static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (locker) {
                    count++;
                }
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (locker) {
                    count++;
                }
            }
        });

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

//        t1.start();
//        t1.join();
//        t2.start();
//        t2.join();

        //保证线程都执行完
        Thread.sleep(1000);

        System.out.println(count);
    }
}

运行结果:

我们先了解一下使用锁的格式:

synchronized(锁对象){
    
}

这里的锁对象可以是任意引用类型的对象,但要保证在解决同一个非原子问题时是同一个对象。

synchronized的使用方法除了上述格式以外,还能用来修饰方法:

这里的锁对象是this。

class Counter{
    int count = 0;

    public synchronized void addCount(){
        count++;
    }
}

public class Test11 {
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Counter counter = new Counter();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.addCount();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.addCount();
            }
        });
        t1.start();
        t2.start();

        Thread.sleep(100);
        System.out.println(counter.count);
    }
}

运行结果:

其实这样的解决方法也就是将局部的任务给串行化,只不过比直接将整个线程串行化来的含蓄,性能降低的少。

可重入

在Java中synchronized具有可重入的特点。

synchronized (locker) {
                    synchronized (locker) {
                        count++;
                    }
                }

我们知道,锁是具有互斥性的,也就是在上锁后是需要解锁后才能让下一个拥有锁对象的任务执行,那上面这段代码就会形成一个死锁的现象,当进入第一个锁后,会遇到第二个锁,但是想要进入第二个synchronized是需要从第一个synchronized中出来的,但是要想从第一个synchronized中出来就需要进入第二个synchronized,所以这就形成了一个死循环,可以叫死锁。

Java的开发人员为了解决这一问题就赋予了synchronized可重入这一属性,也就是在上述情况下不会出现死锁的现象,Java会自动识别出来。

如果有很多层synchronized嵌套的话,当第一次进入的synchronized结束时,这把锁才会解开。

死锁 

上面讲可重入性的时候讲到了死锁这一概念,这里就详细讲讲死锁。

造成死锁有三种情况:

1.一个线程同一个锁加多次,这也是讲述可重入性时举的例子。

2.N个线程,M把锁。

public class Test12 {
    public static void main(String[] args) {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(()->{
            synchronized (locker1){
                try {
                    System.out.println("t1获取了locker1!");
                    //确保t2先拿到locker2
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker2){
                    System.out.println("t1获取了两把锁!");
                }
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (locker2){
                try {
                    System.out.println("t2获取了locker2!");
                    //确保线程t1先拿到locker1
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker1){
                    System.out.println("t2获取了两把锁!");
                }
            }
        });
        t1.start();
        t2.start();

    }
}

运行结果:

进程并没有结束,使用jconsole查看线程状态,发现是BLOCKED,也就锁造成的无上限的阻塞等待。这是因为在线程t1拿到locker1和t2拿到locker2的情况下,t1要想拿到locker2就必须要让t2解锁,而t2要想解锁,就需要拿到locker1,但是locker1在t1手中,所以就形成了死循环,也就构成了死锁。

3.哲学家就餐问题

假设有七个哲学家围着一个桌子吃饭,每两个人中间放一根筷子,这样的话当一个人用筷子吃饭的时候,他两侧的人都没法用餐。

在一种极端情况下,每个哲学家都拿起他左手边的筷子,这样所有哲学家都在等待对方放下筷子,就形成了死锁。

public class Test13 {
    public static void main(String[] args) {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Object locker3 = new Object();
        Object locker4 = new Object();
        Object locker5 = new Object();
        Thread t1 = new Thread(()->{
            synchronized (locker1){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker5){

                }
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (locker2){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker1){

                }
            }
        });
        Thread t3 = new Thread(()->{
            synchronized (locker3){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker2){

                }
            }
        });
        Thread t4 = new Thread(()->{
            synchronized (locker4){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker3){

                }
            }
        });
        Thread t5 = new Thread(()->{
            synchronized (locker5){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker4){

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

各线程状态: 

解决方案:

1.对筷子按顺序进行编号,先拿到左右小的编号的筷子,拿到后再拿左右大的编号的筷子。

刚开始1号先吃到饭,吃完后放下1号和7号筷子,2号拿到1号筷子,7号拿到7号筷子,7号可以就餐,以此类推,所有人都可以完成就餐。

public class Test13 {
    public static void main(String[] args) {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Object locker3 = new Object();
        Object locker4 = new Object();
        Object locker5 = new Object();
        Thread t1 = new Thread(()->{
            synchronized (locker1){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker5){

                }
            }
        });
        Thread t2 = new Thread(()->{
            //保证t1先拿到locker1,如果t2先拿到locker1,还是会形成死锁
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (locker1){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker2){

                }
            }
        });
        Thread t3 = new Thread(()->{
            synchronized (locker2){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker3){

                }
            }
        });
        Thread t4 = new Thread(()->{
            synchronized (locker3){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker4){

                }
            }
        });
        Thread t5 = new Thread(()->{
            synchronized (locker4){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker5){

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

运行结果:

 4.死锁总结:

造成死锁的原因:

1.锁具有互斥性(锁的基本特性)

当一个锁被一个线程获取之后,当别的线程想要获取这个锁的时候,会线程阻塞。

2.锁不可抢占(锁的基本特性)

当这个锁已经被获取时,别的线程是不能强行抢占这个锁的, 必须等待获取。

3.请求和保持

当一个线程已经有至少一个锁的时候,尝试获取别的锁遇到阻塞,这时候该线程也不会放弃原来的锁。

4.循环等待

线程1等待线程2,线程2等待线程3,线程3等待线程4,线程4等待线程5,线程5等待线程1,这样就产生了死循环。

解决方案:

1.把嵌套的锁改为并列的锁。(基于N个线程,M把锁的代码例子)

public class Test14 {
    public static void main(String[] args) {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(()->{
            synchronized (locker1){
                System.out.println("t1拿到locker1!");
//                try {
//                    Thread.sleep(100);
//                } catch (InterruptedException e) {
//                    throw new RuntimeException(e);
//                }
            }
            synchronized (locker2){
                System.out.println("t1拿到locker2!");
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (locker2){
                System.out.println("t2拿到locker2!");
//                try {
//                    Thread.sleep(100);
//                } catch (InterruptedException e) {
//                    throw new RuntimeException(e);
//                }
            }
            synchronized (locker1){
                System.out.println("t2拿到locker1!");
            }
        });
        t1.start();
        t2.start();
    }
}

运行结果:

2.规定加锁顺序编号递增/递减(基于哲学家就餐问题)

代码案例在上述哲学家就餐问题中的Test13.

3.java标准库中的线程安全类:

StringBuffer,Hashtable,Vector,ConcurrentHashMap,String。。。。。。

前三个不推荐使用。

不安全类:
StringBuilder,ArrayList,LinkedList,HashMap,TreeMap,HashSet,TreeSet。

以StringBuffer为例,查看标准库中的代码,发现其内部是由简单加synchronized实现的,当面对比较复杂的情况时,很有可能会出现bug~~

4.wait/notify

在有些情况下我们需要让某个线程处于阻塞状态,在完成某些任务后再进行唤醒。

注意在调用wait/notify时必须实在synchronized的代码块中,并且必须是相同的锁对象才行。

wait下的线程状态:WAITING

public class Test16 {
    public static void main(String[] args) {
        Object locker = new Object();
        Thread t1 = new Thread(()->{
            synchronized (locker) {
                System.out.println("线程t1wait......");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("线程t1被唤醒!");
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (locker) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t2线程尝试唤醒t1......");
                locker.notify();
            }
        });
        t1.start();
        t2.start();
    }
}

运行结果:

在线程wait期间,该线程是主动放弃了CPU资源的,是解锁状态,暂时不会参与锁竞争。

这种情况下,当使用notify唤醒时只能唤醒其中一个,并且是随机的,这就有很大的不确定性在里面,所以java标准库中还提供了notifyAll方法,能够唤醒所有相同锁对象的wait。

对于notify是随即唤醒这一点还有可能会造成线程饿死,所谓线程饿死也就是某个线程长时间没有吃到CPU的资源。

public class Test17 {
    public static void main(String[] args) {
        Object l1 = new Object();
        Thread t1 = new Thread(()->{
            synchronized (l1){
                try {
                    l1.wait();
                    System.out.println("t1被唤醒!");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (l1){
                try {
                    l1.wait();
                    System.out.println("t2被唤醒!");
                }    catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        Thread t3 = new Thread(()->{
            synchronized (l1){
                try {
                    l1.wait();
                    System.out.println("t3被唤醒!");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        Thread t4 = new Thread(()->{
            System.out.println("输入任意内容唤醒所有线程:");
            Scanner sc = new Scanner(System.in);
            sc.next();
            synchronized (l1){
                l1.notifyAll();
            }
        });

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

运行结果:

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值