Java多线程学习二 synchronized

前面讲过,线程共享变量是非线程安全的,synchronized关键字可使方法变为线程安全的方法

一、线程安全问题

    private CountNum countNum;

    public MyThread(CountNum countNum) {
        this.countNum = countNum;
    }

    @Override
    public void run() {
        countNum.add("a");
    }
public class MyThread2 extends Thread {
    private CountNum countNum;

    public MyThread2(CountNum countNum) {
        this.countNum = countNum;
    }

    @Override
    public void run() {
       countNum.add("b");
    }
}

public class TestMain {
    public static void main(String[] args)  {
        CountNum countNum=new CountNum();
        MyThread myThread=new MyThread(countNum);
        MyThread2 myThread2=new MyThread2(countNum);
        myThread.start();
        myThread2.start();
    }
}

输出

a set over
b set over
name:b   num:200
name:a   num:200

两个线程同时访问一个没有同步的方法,a修改完num的值睡眠时,b这时又修改了num的值,a打印时的num值其实已经被b给修改了,这就是共享变量被多线程同事访问时候的问题。

二、synchronized

解决上述问题很简单,只需要在方法前加上synchronized关键字即可,即使a睡眠了,b也不会进入方法执行,此时方法是同步顺序执行的,输出如下

a set over
name:a   num:100
b set over
name:b   num:200

三、多个对象多个锁

把TestMain代码改成如下

public class TestMain {
    public static void main(String[] args)  {
        CountNum countNum=new CountNum();
        CountNum countNum2=new CountNum();
        MyThread myThread=new MyThread(countNum);
        MyThread2 myThread2=new MyThread2(countNum2);
        myThread.start();
        myThread2.start();
    }
}
a set over
b set over
name:b   num:200
name:a   num:100

每个线程用不同的对象,此时代码又变为异步执行的,因为此时synchronized的锁是不同的对象。synchronized关键字取得的锁都是对象锁。
注意:
A线程获得某对象的锁时,其他线程可以以异步的方式调用该对象非synchronized类型的方法,但是其他线程调用其他synchronized的方法时需要等待

四、脏读

public class PublicVar {
    private String name="b";
    private String passwd="bb";

    public synchronized void setValue(String name,String passwd){
        try {
            this.name=name;
            Thread.sleep(5000);
            this.passwd=passwd;
            System.out.println("setName "+Thread.currentThread().getName()+" name="+name+" passwd="+passwd);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public String getValue() {
        System.out.println("getName "+Thread.currentThread().getName()+" name="+name+" passwd="+passwd);
        return name;
    }
}

public class MyThread extends Thread {

    private PublicVar publicVar;

    public MyThread(PublicVar publicVar) {
        this.publicVar = publicVar;
    }

    @Override
    public void run() {
        publicVar.setValue("a", "aa");
    }
}

    public static void main(String[] args) throws InterruptedException {
        PublicVar publicVar=new PublicVar();
        MyThread myThread=new MyThread(publicVar);
        myThread.start();
        Thread.sleep(2000);
        publicVar.getValue();
    }
getName main name=a passwd=bb
setName Thread-0 name=a passwd=aa

此处出现脏读的原因是getValue方法并不是同步的,主线程启动完thread线程后休眠了2秒,thread线程在设置完name的值后休眠了5秒,主线程醒来后继续执行getValue方法,在休眠的两秒时间里,name已经被thread线程设置新值,但passwd还没有,所以出现了这种情况。
解决此问题的方法当然是给getValue方法也加上同步synchronized关键字

总结:
当A线程调用anyObject对象加入synchronized关键字的X方法时,A线程就获得了X方法锁,更准确的将,是获得了对象的锁,所以其他线程必须等A线程执行完毕才可以调用X方法,但其他线程可以随意调用其他非synchronized的方法,而其他线程调用声明synchronized关键字的非X方法时,也必须等A线程将X方法执行完,也就是释放对象锁后才可以调用。

五、synchronized锁重入

可重入锁:比如A线程获得了某对象的锁,此时这个对象锁还没有释放,当其再次想获取这个对象的锁时还说可以获取的,一个synchronized方法/块的内部调用本类或父类的其他synchronized方法/块时,是永远可以得到锁的。如果不可锁重入的话,就会造成死锁。
当存在父子类继承关系时,子类是完全可以通过“可重入锁”调用父类的同步方法的。
注意:

重写父类的synchronized方法不具有同步效果,如需同步仍要手动加上synchronized关键字

六、死锁

public class DealThread implements Runnable {

    public String name;
    public Object lock1 = new Object();
    public Object lock2 = new Object();

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        if ("a".equals(name)) {
            synchronized (lock1) {
                try {
                    System.out.println("name=" + name);
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2) {
                    System.out.println("按lock1->lock2代码顺序执行了");
                }
            }
        }
        if ("b".equals(name)) {
            synchronized (lock2) {
                try {
                    System.out.println("name=" + name);
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1) {
                    System.out.println("按lock2->lock1代码顺序执行了");
                }
            }
        }
    }
}

    public static void main(String[] args) throws InterruptedException {
        DealThread dealThread=new DealThread();
        dealThread.setName("a");
        Thread thread=new Thread(dealThread);
        thread.start();
        Thread.sleep(1000);
        dealThread.setName("b");
        Thread thread2=new Thread(dealThread);
        thread2.start();
    }

查看死锁信息,进入jdk的bin目录执行jps命令,输出如下,发现TestMain线程的id为8196

3904 Jps
10628 Launcher
8196 TestMain
9892 RemoteMavenServer
11432

使用jstack命令查看结果jstack -l 8196

Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x0000000002f1c978 (object 0x00000000d59a1d30, a java.lang.Object),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x0000000002f188d8 (object 0x00000000d59a1d40, a java.lang.Object),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
        at com.yjj.chapter02.test04_syn4.DealThread.run(DealThread.java:43)
        - waiting to lock <0x00000000d59a1d30> (a java.lang.Object)
        - locked <0x00000000d59a1d40> (a java.lang.Object)
        at java.lang.Thread.run(Thread.java:748)
"Thread-0":
        at com.yjj.chapter02.test04_syn4.DealThread.run(DealThread.java:30)
        - waiting to lock <0x00000000d59a1d40> (a java.lang.Object)
        - locked <0x00000000d59a1d30> (a java.lang.Object)
        at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

七、锁对象改变

public class MyService {
    private String lock="123";

    public void testMethod(){
        synchronized (lock){
            try {
                System.out.println(Thread.currentThread().getName()+" begin "+System.currentTimeMillis());
                lock="456";
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName()+" end "+System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class ThreadA extends Thread{
    private MyService myService;

    public ThreadA(MyService myService) {
        this.myService = myService;
    }

    @Override
    public void run() {
        myService.testMethod();
    }
}

ThreadA同ThreadB

public class TestMain {
    public static void main(String[] args) throws InterruptedException {
        MyService service=new MyService();
        ThreadA a =new ThreadA(service);
        a.setName("A");
        ThreadB b =new ThreadB(service);
        b.setName("B");
        a.start();
        Thread.sleep(50);
        b.start();
    }
}

可以看到A线程和B线程是异步执行的,虽然加了synchronized关键字,但是由于A线程竞争的是锁是“123”,得到锁后把锁改为了“456”,而B线程竞争的是“456”,此时并没有线程占用“456”的锁,于是B线程也得到了锁,就出现了如下的情况。

A begin 1537950658147
B begin 1537950658196
A end 1537950660147
B end 1537950660196

要注意的是:只要对象不变,即使对象的属性被改变,运行结果仍是同步的,也就是说把一个User对象当成锁,如果user的name属性发生变化,但是仍然是同一个user的话,仍会同步执行

八、volatile关键字

关于volatile以后写一篇专门的文章吧,现在只需要知道 volatile只保证可见性、有序性,并不保证原子性,i++并不是一个原子操作

九、原子类用不好也不完全安全

public class MyService {
    public static AtomicLong atomicLong=new AtomicLong();

    public void addNum(){
        System.out.println(Thread.currentThread().getName()+"加了100后值="+atomicLong.addAndGet(100));
        atomicLong.addAndGet(1);
    }
}
    @Override
    public void run() {
        myService.addNum();
    }
    public static void main(String[] args) {
        MyService myService=new MyService();
        AddCountThread addCountThread=new AddCountThread(myService);
        Thread t1=new Thread(addCountThread);
        Thread t2=new Thread(addCountThread);
        Thread t3=new Thread(addCountThread);
        Thread t4=new Thread(addCountThread);
        Thread t5=new Thread(addCountThread);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
    }
Thread-1加了100后值=100
Thread-3加了100后值=300
Thread-2加了100后值=200
Thread-4加了100后值=403
Thread-5加了100后值=503

输出结果如上,发现并不是我们想要的结果,造成这个的原因是addAndGet()方法调用不是原子的,虽然这个方法是一个原子方法,但是两个addAndGet()方法之间的调用却不是原子的,解决这一的问题必须要用同步,在addNum方法上synchronized或者用同步代码块包括在两个addAndGet()方法外

你可能感兴趣的:(Java多线程学习二 synchronized)