【JAVA多线程编程核心技术】第二章 对象及变量的并发访问

这里仅仅是我读该书收获的知识点,如果我熟悉的我自动或略不写

一、synchronized可重入锁

在使用synchronized,当一个线程获取某个锁之后,再次请求这个锁,可以再次得到这个锁。
比如线程thread-0获取了对象A的锁,现在还没有释放该锁,然后开始去请求另外一个资源(或者方法),而该资源也需要对象A的锁,如果不可重入,那么可以想象的是会发生死锁

public synchronized void method1(){
	doSomething;
	method2();//虽然method2也需要锁,但是它和method1是同样的锁(this),那么可以再次进入method2方法。
}

public synchronized void method2(){
	doSomething;
}

对于继承同样适用

//程序入口
public static void main(String[] args) {
        MyThread thread1 = new MyThread();

        thread1.start();

    }

//线程类
    private static class MyThread extends Thread{

        @Override
        public void run() {

            B b = new B();
            b.method2();
        }
    }

//父类
private static class A{

        public int i=5;

        public synchronized void method1(){

            try {
                i--;
                System.out.println("A of i equals:"+i);
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

//子类
 private static class B extends A{

        public synchronized void method2(){

            try {
                while(i>0){
                    i--;
                    System.out.println("B of i equals:"+i);
                    Thread.sleep(1000);
                    /**
                     * 这里也可以说明,子类继承父类的时候,子类完全可以通过"可重入锁"调用父类同步方法
                     * 原因也很简单,因为method2方法的锁是this也就是b的一个实例,通过this.method1()调用也将this
                     * 传到父类中了,父类的this此时也指向子类的实例
                     */
                    this.method1();//B还没有释放锁,就去调用父类方法,父类方法如果使用的是同步方法,哪么无需再次获取锁。
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

二、synchronized同步代码块

synchronized同步方法弊端其实很明显,那就是不高效,它把异步方法变成了同步方法。

public class MyThread extends Thread{
	private Object a;
	private Object b;
	
	public synchronized void method(){
		//获取数据库数据操作3s
		Object temp1= getByDatabase1();
		
		//获取数据库数据操作3s
		Object temp2= getByDatabase2();
        
        a = temp1;
        b = temp2;
	}
}

比如上面一个方法,有两个线程去执行,首先线程1进入方法,然后线程2就开始等待,大概6s后线程1会结束,然后线程2开始进入方法,在6s后线程2会结束,这样一共就花费了12s这个方法才结束。
其实观察发现,两个线程共享的数据只有a和b(temp1和tepm2是方法局部变量,不是共享数据),这个时候只有

		a = temp1;
        b = temp2;

是不安全的,我们其实只要保证这部分代码同步即可。

public void method(){
		//获取数据库数据操作3s
		Object temp1= getByDatabase1();
		
		//获取数据库数据操作3s
		Object temp2= getByDatabase2();
		
        synchronized (this){
	  		a = temp1;
	        b = temp2;
		}
	}
}

这样效率提高了一倍,因为synchronized同步代码块中

		//获取数据库数据操作3s
		Object temp1= getByDatabase1();
		
		//获取数据库数据操作3s
		Object temp2= getByDatabase2();

是异步执行的。

2.1同步代码块+分支判断的脏读问题

package com.hfview.SynchronizedBlock;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;

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

/**
 * @Auther: zhw
 * @Date: 2019/3/4 09:41
 *
 * 同步方法的缺点很明显,就是不高效,只要一个方法占用了锁,其他方法就会阻塞。
 * 同步代码块的引入可以部分解决这个问题,让只会出现线程安全的代码进行同步,让费时,但不影响线程安全的代码异步操作。
 * 所以在试用同步代码块的方法中会存在部分同步,部分异步的情况。如果同步代码块结合if判断,很有可能出现脏读问题。
 * 下面例子开始演示
 */
@Slf4j
public class UnSafeDemo {


    @Test
    public void test1(){
        MyService myService = new MyService();

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                myService.add("aaa");
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                myService.add("bbb");
            }
        });

        thread1.start();
        thread2.start();

        try {
            Thread.sleep(5000);
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println(myService.size());
    }

    //只能存一个元素
    @Data
    private class MyService{

        private  List list = new ArrayList<>();

        public synchronized int size(){
            return list.size();
        }

        public synchronized void doAdd(T t){
            list.add(t);
        }

        public void add(T t){
            log.debug(" enter into method add ");

            try {
                if(size()<1){
                    Thread.sleep(2000);
                    doAdd(t);
                }else{
                    log.debug("the list can only set one element");
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }


    }



}

解决方法也很简单,就是把分支语句也同步

2.2数据常量String的常量池特性

String a = "a";
String b = "b";
sout(a==b)

上面的结果是true,这个是JVM针对String常量纳入池缓存所导致的。所以如果采用string作为对象监视器(也就是锁),必须注意,只要值相同,虽然表面不是同一个对象,其实他们是相同的。

2.3死锁

互相等待对方释放锁,就有可能有死锁发生

package com.hfview.DeadLock;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;

/**
 * //TODO 写注释
 *
 * @author: zhw
 * @since: 2019/3/4 10:19
 */
@Slf4j
public class Demo1 {

    public static void main(String[] args) {
        final A a = new A();

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                a.setFlag("a");
                a.run();
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                a.setFlag("b");
                a.run();
            }
        });

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


    @Data
    private static class A {

        private String flag = "";

        private Object lock1 = new Object();
        private Object lock2 = new Object();

        public void method1(){
            try {
                synchronized (lock1){
                    log.debug("get lock1 ");
                    Thread.sleep(2000);

                    synchronized (lock2){
                        log.debug("get lock1 and lock2 ");
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        public void method2(){
            try {
                synchronized (lock2){
                    log.debug("get lock2 ");
                    Thread.sleep(2000);

                    synchronized (lock1){
                        log.debug("get lock2 and lock1 ");
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        public void run() {
            if(flag.equals("a")){
                method1();
            }else if(flag.equals("b")){
                method2();
            }
        }
    }


}

查看死锁的发生情况。
jps命令
查看运行线程的ID值
【JAVA多线程编程核心技术】第二章 对象及变量的并发访问_第1张图片
jstack -l 命令
【JAVA多线程编程核心技术】第二章 对象及变量的并发访问_第2张图片
很明显发生了死锁现象,“Thread-1” 等待锁0x000000001822f6d8,然而该锁被"Thread-0"占有。“Thread-0” 等待锁0x000000001822ca28,然而该锁被"Thread-1"占有。

三、synchronized和volatile的可见性和原子性

这部分我认为有一篇博客讲的比书上精彩文章地址

  • 线程安全包活可见性和原子性
  • 线程分为私有内存和共有内存
  • volatile是synchronize轻量级实现,保证了可见性
  • synchronized既保证可见性又保证原子性

你可能感兴趣的:(Java多线程编程核心技术)