Java-多线程-基础 - && synchronized && volatile

public class T implements Runnable {
    private int cnt = 10;
    @Override
    public void run() {
        cnt--;
        System.out.println(Thread.currentThread().getName() + " cnt = " + cnt);
    }
    public static void main(String[] args){
        T t = new T();
        for (int i = 0; i < 5; i++) {
            new Thread(t, "THREAD " + i).start();
        }
    }
}

输出:

THREAD 1 cnt = 8
THREAD 2 cnt = 7
THREAD 0 cnt = 9
THREAD 3 cnt = 6
THREAD 4 cnt = 5

开了5个线程,每一个线程都去将cnt减一次,但是由于是多线程,可能存在的问题是,当第一个线程执行cnt--的时候,此时还没有打印的时候,第二个线程就又来了,将cnt--,然后才执行了第一个线程得输出,所以第一次输出就出现了8的错误。

并且上面的通过继承Runnable 接口并实现其run 得方法,进行多线程得编程。

synchronized关键字

同步锁,其作用就是将当线程序执行的对像锁住,不让其他线程来执行这个对象,当第一个线程执行结束之后再将这个对象给第二个线程。这样就不会出现上面的现象。

public class T implements Runnable {
    private int cnt = 10;
    @Override
    public synchronized void  run() {
        cnt--;
        System.out.println(Thread.currentThread().getName() + " cnt = " + cnt);
    }
    public static void main(String[] args){
        T t = new T();
        for (int i = 0; i < 5; i++) {
            new Thread(t, "THREAD " + i).start();
        }
    }
}

synchronized关键字锁住的是对象,而不是当前执行的方法代码,如下代码

public class T implements Runnable {
    private int cnt = 10;
    @Override
    public synchronized void  run() {
        cnt--;
        System.out.println(Thread.currentThread().getName() + " cnt = " + cnt);
    }
    public static void main(String[] args){
        for (int i = 0; i < 5; i++) {
            T t = new T();
            new Thread(t, "THREAD " + i).start();
        }
    }
}

执行之后输出的都

THREAD 0 cnt = 9
THREAD 4 cnt = 9
THREAD 3 cnt = 9
THREAD 2 cnt = 9
THREAD 1 cnt = 9

每个线程每次执行都new 一个新的对象,每个线程都去锁住new 出来的t,共5个,但是并不知道哪个线程先被cpu执行,所以THREAD乱的。

如下程序:

public class T  {
    public synchronized void m1() {
        System.out.println(Thread.currentThread().getName() + " m1 start");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {e.printStackTrace();}
        System.out.println(Thread.currentThread().getName() + " m1 end");
    }
    public void m2() {
        System.out.println(Thread.currentThread().getName() + " m2 start");
    }
    public static void main(String[] args){
        T t = new T();
        new Thread(() -> t.m1(), "m1").start();
        new Thread(() -> t.m2(), "m2").start();
    }
}
输出:
m1 m1 start
m2 m2 start
m1 m1 end

这个程序主要说明的地方是

  1. 使用 new Thread(() -> t.m1(), "m1").start(); 创建多线程得编码方式。还可以写为 new Thread(t::m1(), "m1").start();
  2. 在同步锁方法执行的过程中,还是可以去执行非同步的方法的。在第一个线程同步方法m1休眠得时候,第二个线程又去执行非同步方法。也就是说多个线程执行加同步锁方法的时候会排队,但是多个线程中加同步锁和不加同步锁的方法执行的时候不会存在等待机制。

如下最典型的例子,同步锁去写数据,非同步锁去读数据,读出来得数据并不是我们所希望的。

public class Account {
    private String name;
    private Integer balance=0;
    public synchronized void set(String name, Integer balance) {
        this.name = name;
        try {  Thread.sleep(2000);
        } catch (InterruptedException e) { e.printStackTrace();}
        this.balance = balance;
    }
    public Integer getBalance() {return balance;}
    public static void main(String[] args){
        Account account = new Account();
        new Thread(() -> account.set("zhangsan", 1000)).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {e.printStackTrace();}
        System.out.println("第一次读取 "+account.getBalance());
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {e.printStackTrace();}
        System.out.println("第二次读取 "+account.getBalance());
    }
}
输出结果:
第一次读取 0
第二次读取 1000

这就是上面提到的『脏读』的问题,set 修改balance得时候此时还没有set值得时候,getBalance 来读取它,结果是0,二第二次读取的时候,已经赋值了,就是1000,这就是多线程同步锁只会取锁住set 方法,但是在执行这个方法的时候,其他非同步锁得方法还是可以执行的,

解决上面『脏读』问题方法是在 getBalance 方法上也加一把锁,这样,set 方法在执行的时候锁住了 account对象,getBalnce方法就不能去执行了,直到第一个线程结束

一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁.

public class Account {
    private synchronized void m1(){
        System.out.println("m1 start");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        m2();
    }
    private synchronized void m2(){
        System.out.println("m2 start");
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args){
        Account account = new Account();
        new Thread(() ->account.m1()).start();
    }
}

对t执行m1的时候,需要在t上面加把锁,拿到这个锁了,开始执行,执行锁定的过程之中,调用了m2();调用m2的过程中,发现m2也是需要申请一把锁,而申请的这把锁就是当前自己已经持有的这把锁;严格来讲,这把锁m1已经持有了,m2还能持有吗?由于是在同一个线程里面,这个是没关系的。它可以再去申请我自己已经拥有的这把锁,实际上就在这把锁上加个数字,从1变成2,锁定了2次。总而言之,再去申请当前持有的这把锁没问题,仍然会得到该对象的锁。一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁,也就是说synchronized获得的锁是可重入的。

重入锁的另外一种情形,继承中子类的同步方法调用父类的同步方法,结论和上面的都是一样的。

public class Account {
     synchronized void m1(){
        System.out.println("m1 start");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args){
        TT t = new TT();
        new Thread(() ->t.m()).start();
    }
}
class TT extends Account{
    synchronized void m(){
        System.out.println("child m start");
        super.m1();
        System.out.println("child m end");
    }
}
输出:
child m start
m1 start
child m end

synchronized 同步方法如果遇到异常就会释放。所以在并发处理的时候,需要异常的小心,不然就会产生不一致得问题,如在web app中多个servlet多线程共同访问同一个资源的时候,如果异常处理不合适,那么当第一个线程抛出异常,而其他就会静茹同步代码区(之前那个释放了,所以其他可以进入),可能会访问到上次只处理到一半的有问题的数据。

public class Account {

    private int cnt=0;
     synchronized void m1(){
        while (true){
            cnt ++;
            System.out.println(Thread.currentThread().getName() + ": " + cnt);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (cnt==5){
                int i=1/0;
                // 这里会抛出异常,这个线程就会结束
            }

            // 处理异常,catch住,然后不释放锁
            //if (cnt==5){
            //    try {
            //    int i=1/0;
            //    // 这里会抛出异常,这个线程就会结束
            //    }catch (Exception e){
            //        System.out.println(e.getMessage());
            //    }
            //}
        }
    }
    public static void main(String[] args){
        Account t = new Account();
        new Thread(() ->t.m1(), "t1").start();

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() ->t.m1(), "t2").start();
    }
}
输出:
t1: 1
t1: 2
t1: 3
t1: 4
t1: 5
t2: 6
Exception in thread "t1" java.lang.ArithmeticException: / by zero
	at Basic.Account.m1(Account.java:21)
	at Basic.Account.lambda$main$0(Account.java:39)
	at java.lang.Thread.run(Thread.java:748)
t2: 7
处理异常后的输出
t1: 1
t1: 2
t1: 3
t1: 4
t1: 5
/ by zero
t1: 6
t1: 7

这里打印就可以看出,这种因为异常二导致的线程退出,线程t1可能是在处理某事情,例如修改数据库,数据修改列一半结果就遇到异常退出了,而第二个线程如果是来读第一个线程修改了的东西,那么就会 『脏读』,所以处理异常是一个非常腰小心的事情。

一个处理的方法就是,当抛出异常之后去catch,让锁不释放,继续执行。

volatile关键字

public class Account {
    volatile boolean running = true;
    void m(){
        System.out.println("m start");
        while (running){
        }
        System.out.println("m end");
    }
    public static void main(String[] args){
        Account account = new Account();
        new Thread(() -> account.m()).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        account.running=false;
    }
}

volatile 关键字,是一个变量在多个线程间可见,加入现在有A,B两个线程都会使用到同一个变量,那么java默认是A线程保留一份copy,这样如果B线程修改列该变量,则A线程未必直到,而使用volatile关键字,会让所有线程都会读到变量修改。在执行每个线程得时候,每个线程都会去堆内存中去读取running值,这个功能在使用synchronized也是有的,但是volatile并不会去锁住对象,也就是说volatile 是要比synchronized要轻量。

不加volatile得情况下,上面程序执行的过程是这样的:

线程之间要让running这个值进行可见,这里要涉及到java的内存模型,java对于线程处理的内存模型;

在jmm(java memory model)里面有个内存它叫主内存,我们所熟识的栈内存,堆内存都可以认为是主内存;每一个线程在执行的过程之中,它有一个线程自己的一块内存,(实际上不能认为这块是内存,有可能它是内存,还有cpu上的缓冲区,是一个统称,就是线程存放它自己变量的一块内存),如果两个cpu在运行不同线程的话,每个线程上都有自己的一块缓冲区,缓冲区就是把主内存JMM里面的内容读过来在缓冲区里面进行修改,如果+1,+1加了好多次再写回去;

现在有个running在主内存里面,值是true,占一个字节;

第一个线程启动的时候会把这个字节copy到自己的缓冲区里面,cpu在处理的过程之中就不再去主内存里面读了;它在运行这个线程的过程之中,由于这个cpu非常的忙,在while(running)里面,没空再去主线程里面去刷一下running值了;它一直读自己缓存里面的内容,running永远是true;

第二个主线程里面,它首先也是把running读到它自己的缓冲区,然后把running改成false,发现running已经改了那就把running写回到主内存里面去;写回到主内存之后,但是第一个线程它没有在主内存重新读啊,所以第一个线程永远结束不了;

加了volatile之后的情况是这样的

加了volatile,第一个线程运行中,不是要求你每次while(running)循环的时候都要到主内存里面读一次running的值,而是说一旦主内存running这个值发生改变后会通知别的线程,说你们的缓冲区里面内容过期了请重新读一下,第一个线程再去读的时候running已经改了,所以线程结束了。

加了volatile的意思就是当running改了后会通知其他的所有线程的缓冲区,说你们那边的值已经过期了,请你们再去主内存里面重新读一下。

而并不是通知所有的线程cpu执行的时候每次用的时候都要去主内存读一下,不是,是写完之后进行缓存过期通知。

要保证线程之间的可见性,那么需要对两个线程共同访问的变量加上volatile;如果不想加volatile那只能用synchronized;但volatile的效率要比synchronized高的多;所以在很多高并发的框架里面好多的volatile关键字都在用;比如JDK的并发容器的源码;能用volatile的时候就不要加锁,程序的并发性就要提高很多;

volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,

也就是说volatile不能替代synchronized。

volatile只保证可见性,并不保证原子性。(原子性计算不可以在拆分 如:int i = 4;但是 a+=1; 这就是可拆分)

synchronized既保证可见性,又保证原子性;但效率要比volatile低不少。

如果只需要保证可见性的时候,使用volatile,不要使用synchronized。

synchronized得优化

使用synchrinized 得同步代码快中的语句越少越好。

如在上面的代码中都是对整个方法进行使用synchrinized修饰,可以改进为:

synchronized(this) {
            count ++;
}

这样同步代码块,这时候不应该给整个方法上锁,采用细粒度得锁,使得线程争用得时间变短,从而提高效率。

应该避免锁定对象的引用变为另外的对象

public class Account {
    volatile boolean running = true;

    Object o = new Object();

    void m(){
        synchronized (o){
            while (true){
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
            }
        }
    }

    public static void main(String[] args){
        Account t = new Account();

        new Thread(t::m, "t1").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Thread t2 = new Thread(t::m, "t2");
        t.o = new Object();  ////--------****------////

        t2.start();
    }

在有 * 标注的那行,将锁的对象切换,从事是的线程2开始执行,如果对象没有发送改变,那么线程2将不会得到执行,锁的对象发生改变,就不需要锁原来的对象,直接锁新对象就行了;而新对象还没有锁的,所以t2线程就被执行了;这就证明这个锁是锁在什么地方?是锁在堆内存里new出来的对象上,不是锁在栈内存里头o的引用,不是锁的引用,而是锁new出来的真正的对象;锁的信息是记录在堆内存里的

在多线程编程得时候,需要注意的是,不要以字符常量作为锁得对象。

public class T {   
    String s1 = "Hello";
    String s2 = "Hello";
    void m1() {
        synchronized(s1) {           
        }
    }   
    void m2() {
        synchronized(s2) {
            
        }
    }
}
 * 不要以字符串常量作为锁定对象
 * 在下面的例子中,m1和m2其实锁定的是同一个对象
 * 这种情况还会发生比较诡异的现象,比如你用到了一个类库,在该类库中代码锁定了字符串“Hello”,
 * 但是你读不到源码,所以你在自己的代码中也锁定了"Hello",这时候就有可能发生非常诡异的死锁阻塞,
 * 因为你的程序和你用到的类库不经意间使用了同一把锁

上面提到的,同步锁锁得是堆内存里面new 出来的对象,虽然m1和m2是两个变量,但是指向得是到堆内存中的同一个对象

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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