多线程初阶(二)

目录

前言:

synchronized

解析

可重入和不可重入问题

解析

Java中线程安全类

死锁问题

解析

解决死锁问题

解析

内存可见性

解析

volatile关键字

解析

wait,notify

解析

小结:


前言:

    针对上篇文章讲到的线程安全问题,我们需要保证一些指令的原子性,在代码中可以通过加锁实现。针对于加锁,这个是有一定的开销的,还有可能导致死锁问题。因此在加锁的时候要慎重考虑。

synchronized

    1)修饰普通方法是把锁加到当前引用对象上。

    2)修饰静态方法是把锁加到类对象上。

    3)修饰代码块,可以指定加到哪个对象上。

注意:

    如果两个线程针对同一个对象加锁,就会出现锁竞争/冲突,一个线程能够获得到锁,先到的线程先获得锁。另一个线程则需要阻塞等待,直到上一个线程解锁(方法执行完),这个线程就可以回到就绪队列,才能够获取到锁。

    这里加锁虽然在方法上修饰,但实际加锁都是加到对象上面的。只有两个线程针对同一个对象加锁,才会出现锁冲突。如果针对不同对象加锁,则都会获取到锁,不会产生阻塞等待。

class Cumsum {
    public int a = 0;
    synchronized public void add() {
        a++;
    }
}
public class ThreadDemo15 {
    public static void main(String[] args) {
        Cumsum cumsum = new Cumsum();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 5000; i++) {
                     cumsum.add();
                }

            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 5000; i++) {
                    cumsum.add();
                }
            }
        });

        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(cumsum.a);

    }
}

解析

    上篇讲述到这段代码,不加锁运行结果是有bug的,加了锁之后就正确了。不加锁出bug的原因在上片文章有讲述到,就是典型的线程安全问题。那么为什么加了锁之后代码就是正确呢?

    首先这里有两个线程t1和t2,main线程会阻塞等待,这两个线程并发执行。如果第一个线程首先获取到锁,这个锁是加到cumsum对象上的,当第二个线程在尝试获取到这个对象的锁,就会产生阻塞等待(对象是同一个)。直到上一个线程释放了这个对象的锁,这个线程才可以获取这个锁成功。获取锁成功后读取到a的值肯定是save过的,即就是正确的数值。

可重入和不可重入问题

    如果一个线程在一个方法里尝试针对同一个对象加锁两次,第一次加锁成功后,第二次在尝试对这个方法加锁,就会产生阻塞等待。即就会阻塞在这个方法里,第一次加的锁没有办法释放,程序就会一直阻塞在这里,产生死锁问题。

    针对给一个对象加锁两次产生的死锁问题,在java里很可能会写出这样的代码,因此synchronized就设计为可重入锁。对于这样死锁现象,可重入锁就不会产生阻塞等待,就会放过它,即代码可以正常执行。对于这样原因产生死锁问题,即就是不可重入锁。

class Add {
    public static int a = 0;
    synchronized public  void add() {
        a++;
    }
    synchronized public void add2() {
        synchronized (this) {
           a++;
       }
    }
}
public class ThreadDemo11 {

    public static void main(String[] args) throws InterruptedException {
        Add add2 = new Add();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 5000; i++) {
                    add2.add();
                }
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 5000; i++) {
                    add2.add2();
                }
            }
        });
        //执行一次a++需要,load,add,save(都内存数据到寄存器,寄存器值++, 寄存器值写回内存)
        //由于两个线程是并发执行的,这些指令会随机组合(抢占式执行,随意调度),就会产生线程安全问题(不同的顺序,结果就会产生差异)
        //第二个线程读取的值是在第一个线程保存后读取的1,就会加2次(线程安全)
        //两次读取的值都为0,则最终只加1. 在一次线程切换中,另一个线程可能会执行多次三步流程(线程不安全)
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(Add.a);
    }
}

解析

    如果第一个线程先执行,会给add2对象加锁成功。这个时候第二个线程在尝试对于这个对象加锁就会产生阻塞等待。当第一个线程释放锁之后,第二个线程就会针对于这个对象加锁成功,执行代码块的时候又会针对这个对象加锁第二次,由于synchronized是可重入锁,即在这里不会产生死锁问题,即代码就会正常执行。

Java中线程安全类

    Vector  HashTable  ConcurrentHashMap  StringBuffer 这些集合类中都内置了synchronized锁,多线程中线程是安全的。String类由于不可修改性,即天然就是线程安全的。

    AyyayList  LinkedList  HashMap  TreeMap  HashSet  TreeSet  StringBuilder 这些集合类中在线程安全问题需要手动加锁。

死锁问题

    上面说的在一个线程里给一个对象加两次锁,如果锁是不可重入锁,那么就会产生死锁。如果线程1先获取到锁A,被调度走,线程2先获取到锁B,再尝试获取锁A,就会阻塞等待,线程1调度回来获取锁B,也会阻塞等待。这个时候两个线程都在等对方释放锁,程序就会卡着不动了,产生了死锁问题。多个线程多把锁,如果每个线程都获取到锁,并且都在等对方释放锁,那么每个线程都会卡着不动,产生死锁问题。

    死锁问题的核心就是循环等待,想要解决死锁问题,那么就需要打破这种循环等待。

public class ThreadDemo12 {
    public static void main(String[] args) {
        Object o1 = new Object();
        Object o2 = new Object();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (o1) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (o2) {
                        System.out.println("aaaa");
                    }
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (o2) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (o1) {
                        System.out.println("bbbbb");
                    }
                }
            }
        });
        t1.start();
        t2.start();
    }
}

解析

    线程1先个o1对象加锁,然后sleep(),线程2给o2加锁,然后sleep()。接下来线程1尝试获取o2的锁,线程2尝试获取o1的锁,即两个线程都会阻塞等待,产生死锁。

解决死锁问题

    给锁编号,约定获取锁的顺序,从小到大或者从大到小。任意线程在加锁的时候都遵循这样的规则,就可以打破循环等待的问题,那么死锁问题也就解决了。

public class ThreadDemo12 {
    public static void main(String[] args) {
        Object o1 = new Object();
        Object o2 = new Object();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (o1) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (o2) {
                        System.out.println("aaaa");
                    }
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (o1) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (o2) {
                        System.out.println("bbbbb");
                    }
                }
            }
        });
        t1.start();
        t2.start();
    }
}

 解析

    线程1先获取o1再获取o2的锁,线程2也遵循这样的规则。调整了获取锁的顺序后,可以清楚看见代码正常执行了。

内存可见性

    如果针对于同一个变量即读又写,那么就会涉及内存可见性问题。实质上是编译器优化导致的bug。

    对于一个变量的修改,首先需要读取内存的数据到寄存器中,然后在寄存器中修改这个变量,最终写回到内存中。编译器优化可能会认为这个变量是不可变的,即在每次读数据的时候,只读取寄存器中的值,而不是修改后内存中的数据。这就导致读的数据就是修改之前的。

    一个线程针对于一个变量进行修改操作,同时另一个线程针对这个变量读取操作。此时读取到的值不一定是修改后的值,这个线程没有感知到这个变量的变化。

class Counter {
    //不能修饰局部变量
    //局部变量在不同的线程里占用不同的栈空间,意味这就是不同的变量(每个线程都有自己的栈空间)
    public int flag = 0;
}
public class ThreadDemo16 {
    public static void main(String[] args) {

        Counter counter = new Counter();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (counter.flag == 0) {

                }
                System.out.println("aaaa");
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                Scanner scanner = new Scanner(System.in);
                counter.flag = scanner.nextInt();
            }
        });
        t1.start();
        t2.start();
    }
}

解析

    当t2线程修改掉flag值为2时,t1线程在while中死循环。因为t1线程读取到的值是没有修改的flag。这就是编译器优化导致认为flag是不可变的,即每次都是读取寄存器中的值,而不是t2线程修改后内存中的值。 

volatile关键字

    解决内存可见性,使用volatile关键字。声明这个变量是可变的,即告诉编译器在每次读取数据时,需要读取内存中的数据,而不是寄存器中的数据。这个时候编译器就不会随便优化了。

class Counter {
    //不能修饰局部变量
    //局部变量在不同的线程里占用不同的栈空间,意味这就是不同的变量(每个线程都有自己的栈空间)
    volatile public int flag = 0;
}
public class ThreadDemo16 {
    public static void main(String[] args) {

        Counter counter = new Counter();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (counter.flag == 0) {

                }
                System.out.println("aaaa");
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                Scanner scanner = new Scanner(System.in);
                counter.flag = scanner.nextInt();
            }
        });
        t1.start();
        t2.start();
    }
}

 解析

    当给flag加上volatile关键字声明是可变的之后,while循环后的语句顺利打印了,说明t1线程读取到了t2线程修改后的值。因为voiatile修饰后,就会认为这个变量是可变的,即每次都会同步内存中的数据,即也就解决了内存可见性问题。

wait,notify

    由于线程的抢占式执行,随机调度。wait可以让线程主动放弃cpu的调度,进入阻塞队列。让其他线程可以被调度,可以控制线程的调度时机。当使用wait主动放弃cpu的时候,需要其他线程通过notify来唤醒该线程,进入就绪队列。wait和notify都是Object类下的方法。

    wait主动放弃cpu调度的机制,首先会释放锁,然后线程阻塞等待。那么在释放锁的时候需要有锁,即需要先得到锁然后在释放锁,进入阻塞队列。为什么这样设定呢?释放锁之后其他线程可以给这个对象加锁,就不会导致这个对象一直被加锁。wait不加任何参数就是死等。某个线程调用wait方法,就会进入阻塞队列(无论是哪个对象),此时就处在WAITING状态。

    notify通知线程唤醒机制。再唤醒线程也需要获得锁才可以唤醒线程。即首先需要获取锁,然后调用notify方法,唤醒线程进入就绪队列。notify只能唤醒同一个对象调用wait所阻塞的线程,如果有多个线程都在阻塞,则随机唤醒一个。notifyAll可以全部唤醒,一起进入就绪队列。这里的notify唤醒wait不会有任何异常。

public class ThreadDemo17 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("t1前");
                synchronized (object) {
                    try {
                        object.wait(); //不加任何参数就是死等
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("t1后");
            }
        });

        //notify只能唤醒同一个对象上的等待线程
        //如果和wait对象不一致,则不生效
        //多个线程wait的时候,notify随机唤醒一个,notifyAll全部唤醒,一起竞争锁
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("t2前");
                synchronized (object) {
                    object.notify();
                }
                System.out.println("t2后");
            }
        });
        t1.start();
        Thread.sleep(500);
        t2.start();
    }
}

解析

    需要保证先启动t1线程通过synchronized先给object加上锁,然后通过wait方法释放锁,使该线程阻塞等待。当t2线程执行的时候,也是通过synchronized先给object加上锁,然后object对象调用notify方法通知t1线程,进入就绪队列。可以看见代码的执行顺序也是这样。这里wait和notify方法的调用对象需要一致,才能明确具体通知哪一个线程。

小结:

    与大家共勉歌德的名言:志向和热爱是伟大行为的双翼。 

  • 21
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 16
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小小太空人w

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值