Java多线程4:synchronized锁机制

脏读java

一个常见的概念。在多线程中,不免会出如今多个线程中对同一个对象的实例变量进行并发访问的状况,若是不作正确的同步处理,那么产生的后果就是"脏读",也就是取到的数据实际上是被更改过的。安全

 

多线程线程安全问题示例多线程

看一段代码:并发

public class ThreadDomain13
{
    private int num = 0;
    
    public void addNum(String userName)
    {
        try
        {
            if ("a".equals(userName))
            {
                num = 100;
                System.out.println("a set over!");
                Thread.sleep(2000);
            }
            else
            {
                num = 200;
                System.out.println("b set over!");
            }
            System.out.println(userName + " num = " + num);
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

写两个线程分别去add字符串"a"和字符串"b":异步

public class MyThread13_0 extends Thread
{
    private ThreadDomain13 td;
    
    public MyThread13_0(ThreadDomain13 td)
    {
        this.td = td;
    }
    
    public void run()
    {
        td.addNum("a");
    }
}
public class MyThread13_1 extends Thread
{
    private ThreadDomain13 td;
    
    public MyThread13_1(ThreadDomain13 td)
    {
        this.td = td;
    }
    
    public void run()
    {
        td.addNum("b");
    }
}

写一个主函数分别运行这两个线程:函数

public static void main(String[] args)
{
    ThreadDomain13 td = new ThreadDomain13();
    MyThread13_0 mt0 = new MyThread13_0(td);
    MyThread13_1 mt1 = new MyThread13_1(td);
    mt0.start();
    mt1.start();
}

看一下运行结果:学习

a set over!
b set over!
b num = 200
a num = 200

按照正常来看应该打印"a num = 100"和"b num = 200"才对,如今却打印了"b num = 200"和"a num = 200",这就是线程安全问题。咱们能够想一下是怎么会有线程安全的问题的:this

  1. mt0先运行,给num赋值100,而后打印出"a set over!",开始睡觉
  2. mt0在睡觉的时候,mt1运行了,给num赋值200,而后打印出"b set over!",而后打印"b num = 200"
  3. mt1睡完觉了,因为mt0的num和mt1的num是同一个num,因此mt1把num改成了200了,mt0也没办法,对于它来讲,num只能是100,mt0继续运行代码,打印出"a num = 200"

分析了产生问题的缘由,解决就很简单了,给addNum(String userName)方法加同步便可:spa

public class ThreadDomain13
{
    private int num = 0;
    
    public synchronized void addNum(String userName)
    {
        try
        {
            if ("a".equals(userName))
            {
                num = 100;
                System.out.println("a set over!");
                Thread.sleep(2000);
            }
            else
            {
                num = 200;
                System.out.println("b set over!");
            }
            System.out.println(userName + " num = " + num);
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

看一下运行结果:线程

a set over!
a num = 100
b set over!
b num = 200

 

多个对象多个锁

在同步的状况下,把main函数内的代码改一下:

public static void main(String[] args)
{
    ThreadDomain13 td0 = new ThreadDomain13();
    ThreadDomain13 td1 = new ThreadDomain13();
    MyThread13_0 mt0 = new MyThread13_0(td0);
    MyThread13_1 mt1 = new MyThread13_1(td1);
    mt0.start();
    mt1.start();
}

看一下运行结果:

a set over!
b set over!
b num = 200
a num = 100

打印结果的方式变了,打印的顺序是交叉的,这又是为何呢?

这里有一个重要的概念。关键字synchronized取得的锁都是对象锁,而不是把一段代码或方法(函数)看成锁,哪一个线程先执行带synchronized关键字的方法,哪一个线程就持有该方法所属对象的锁,其余线程都只能呈等待状态。可是这有个前提:既然锁叫作对象锁,那么势必和对象相关,因此多个线程访问的必须是同一个对象

若是多个线程访问的是多个对象,那么Java虚拟机就会建立多个锁,就像上面的例子同样,建立了两个ThreadDomain13对象,就产生了2个锁。既然两个线程持有的是不一样的锁,天然不会受到"等待释放锁"这一行为的制约,能够分别运行addNum(String userName)中的代码。

 

synchronized方法与锁对象

上面咱们认识了对象锁,对象锁这个概念,比较抽象,确实不太好理解,看一个例子,在一个实体类中定义一个同步方法和一个非同步方法:

public class ThreadDomain14_0
{
    public synchronized void methodA()
    {
        try
        {
            System.out.println("Begin methodA, threadName = " + 
                    Thread.currentThread().getName());
            Thread.sleep(5000);
            System.out.println("End methodA, threadName = " + 
                    Thread.currentThread().getName() + ", end Time = " + 
                    System.currentTimeMillis());
        } 
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
    
    public void methodB()
    {
        try
        {
            System.out.println("Begin methodB, threadName = " + 
                    Thread.currentThread().getName() + ", begin time = " + 
                    System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println("End methodB, threadName = " + 
                    Thread.currentThread().getName());
        } 
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

一个线程调用其同步方法,一个线程调用其非同步方法:

public class MyThread14_0 extends Thread
{
    private ThreadDomain14_0 td;
    
    public MyThread14_0(ThreadDomain14_0 td)
    {
        this.td = td;
    }
    
    public void run()
    {
        td.methodA();
    }
}
public class MyThread14_1 extends Thread
{
    private ThreadDomain14_0 td;
    
    public MyThread14_1(ThreadDomain14_0 td)
    {
        this.td = td;
    }
    
    public void run()
    {
        td.methodB();
    }
}

写一个main函数去调用这两个线程:

public static void main(String[] args)
{
    ThreadDomain14_0 td = new ThreadDomain14_0();
    MyThread14_0 mt0 = new MyThread14_0(td);
    mt0.setName("A");
    MyThread14_1 mt1 = new MyThread14_1(td);
    mt1.setName("B");
    mt0.start();
    mt1.start();
}

看一下运行效果:

Begin methodA, threadName = A
Begin methodB, threadName = B, begin time = 1443697780869
End methodB, threadName = B
End methodA, threadName = A, end Time = 1443697785871

从结果看到,第一个线程调用了实体类的methodA()方法,第二个线程彻底能够调用实体类的methodB()方法。可是咱们把methodB()方法改成同步就不同了,就不列修改以后的代码了,看一下运行结果:

Begin methodA, threadName = A
End methodA, threadName = A, end Time = 1443697913156
Begin methodB, threadName = B, begin time = 1443697913156
End methodB, threadName = B

从这个例子咱们得出两个重要结论:

  1. A线程持有Object对象的Lock锁,B线程能够以异步方式调用Object对象中的非synchronized类型的方法
  2. A线程持有Object对象的Lock锁,B线程若是在这时调用Object对象中的synchronized类型的方法则须要等待,也就是同步

 

synchronized锁重入

关键字synchronized拥有锁重入的功能。所谓锁重入的意思就是:当一个线程获得一个对象锁后,再次请求此对象锁时能够再次获得该对象的锁。看一个例子:

public class ThreadDomain16
{
    public synchronized void print1()
    {
        System.out.println("ThreadDomain16.print1()");
        print2();
    }
    
    public synchronized void print2()
    {
        System.out.println("ThreadDomain16.print2()");
        print3();
    }
    
    public synchronized void print3()
    {
        System.out.println("ThreadDomain16.print3()");
    }
}
public class MyThread16 extends Thread
{
    public void run()
    {
        ThreadDomain16 td = new ThreadDomain16();
        td.print1();
    }
}
public static void main(String[] args)
{
    MyThread16 mt = new MyThread16();
    mt.start();
}

看一下运行结果:

ThreadDomain16.print1()
ThreadDomain16.print2()
ThreadDomain16.print3()

看到能够直接调用ThreadDomain16中的打印语句,这证实了对象能够再次获取本身的内部锁。这种锁重入的机制,也支持在父子类继承的环境中

 

异常自动释放锁

最后一个知识点是异常。当一个线程执行的代码出现异常时,其所持有的锁会自动释放。模拟的是把一个long型数做为除数,从MAX_VALUE开始递减,直至减为0,从而产生ArithmeticException。看一下例子:

public class ThreadDomain17
{
    public synchronized void testMethod()
    {
        try
        {
            System.out.println("Enter ThreadDomain17.testMethod, currentThread = " + 
                    Thread.currentThread().getName());
            long l = Integer.MAX_VALUE;
            while (true)
            {
                long lo = 2 / l;
                l--;
            }
        } 
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }
}
public class MyThread17 extends Thread
{
    private ThreadDomain17 td;
    
    public MyThread17(ThreadDomain17 td)
    {
        this.td = td;
    }
    
    public void run()
    {
        td.testMethod();
    }
}
public static void main(String[] args)
{
    ThreadDomain17 td = new ThreadDomain17();
    MyThread17 mt0 = new MyThread17(td);
    MyThread17 mt1 = new MyThread17(td);
    mt0.start();
    mt1.start();
}

看一下运行结果:

Enter ThreadDomain17.testMethod, currentThread = Thread-0
Enter ThreadDomain17.testMethod, currentThread = Thread-1
java.lang.ArithmeticException: / by zero
    at com.xrq.example.e17.ThreadDomain17.testMethod(ThreadDomain17.java:14)
    at com.xrq.example.e17.MyThread17.run(MyThread17.java:14)
java.lang.ArithmeticException: / by zero
    at com.xrq.example.e17.ThreadDomain17.testMethod(ThreadDomain17.java:14)
    at com.xrq.example.e17.MyThread17.run(MyThread17.java:14)

由于打印结果是静态的,因此不是很明显。在l--前一句加上Thread.sleep(1)结论会更明显,第一句打出来以后,整个程序都停住了,直到Thread-0抛出异常后,Thread-1才能够运行,这也证实了咱们的结论。

 

后记

文章里面的这些个结论,记一下都是很快的,可是是否记一下就行了?我认为记住这些结论一点都不重要,重要的应该是学习如何经过代码去验证这些结论。由于只有知道了如何经过代码去验证结论,才能够说真正对于synchronized关键字的各类细节有了感性、有了深刻的理解,之后碰到其余synchronized的场景就能够以本身的理解去正确分析问题。

相关文章
相关标签/搜索