多线程之互斥锁(synchronized关键字)

synchronized关键字经常被用来做线程互斥锁,但是使用不当的话,经常达不到目的。初学者常对锁住的是对象还是类有疑问。
原理:无论是对象还是类都有唯一的锁,synchronized只是声明了函数调用时需要什么锁,每个锁同一时间只能由一个线程获取,借此实现了线程互斥。
(1)分析对象锁
A.synchronized修饰非静态函数
接下来看实例:

public enum Person {
    Alice,
    Bob;

    public synchronized void say() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " say hello world! " +TimeUtil.getCurrentTime());
    }

    public synchronized void speak() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " speak hello world! " + TimeUtil.getCurrentTime());
    }

    public void print() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " print hello world! " + TimeUtil.getCurrentTime());
    }

}

这是一个Person类,其中有三个函数,say()和speak()这种声明方式意味着调用该函数需要持有Person实例的对象锁,即用哪个实例调用,则需要持有哪个对象的锁。print()函数无需任何锁。

public class ThreadA implements Runnable {
    @Override
    public void run() {
        Person.Alice.say();
    }
}

public class ThreadB implements Runnable {
    @Override
    public void run() {
        Person.Bob.say();
    }
}

public class ThreadC implements Runnable {
    @Override
    public void run() {
        Person.Alice.print();
    }
}

public static String getCurrentTime() {
    SimpleDateFormat sdf = new SimpleDateFormat("mm:ss");
    return sdf.format(new Date());
}

创建多个线程,然后执行函数:

public class Main {
    public static void main(String[] args) {
        Thread threadA = new Thread(new ThreadA());
        threadA.setName("threadA");
        Thread threadB = new Thread(new ThreadB());
        threadB.setName("threadB");
        Thread threadC = new Thread(new ThreadC());
        threadC.setName("threadC");
        threadA.start();
        System.out.println("A启动了 " + TimeUtil.getCurrentTime());
        threadB.start();
        System.out.println("B启动了 " + TimeUtil.getCurrentTime());
        threadC.start();
        System.out.println("C启动了 " + TimeUtil.getCurrentTime());
    }
}

输出结果为:
A启动了 20:26
B启动了 20:26
C启动了 20:26
threadA say hello world! 20:28
threadC print hello world! 24:21
threadB say hello world! 20:28

可以看出,用两个实例分别调用say()函数是不会出现互斥的。函数执行时,每个函数都可以拿到调用对象的锁。
接下来我们进行改动,将ThreadB改为:

public class ThreadB implements Runnable {
    @Override
    public void run() {
        Person.Alice.say();
    }
}

执行输出:
A启动了 25:33
B启动了 25:33
C启动了 25:33
threadA say hello world! 25:35
threadC print hello world! 25:35
threadB say hello world! 25:37
可以明显看出线程B和A存在对象锁竞争,A持有Alice锁的时候,B等待。
改动ThreadB为

public class ThreadB implements Runnable {
    @Override
    public void run() {
        Person.Alice.speak();
    }
}

执行输出:
A启动了 27:29
B启动了 27:29
C启动了 27:29
threadA say hello world! 27:31
threadC print hello world! 27:31
threadB speak hello world! 27:33
可以看出即使用Alice调用不同的函数,还是会出现等待.
总结:对象锁同一时间只能由一个线程持有,此时其余线程无法再用此对象调用需要此对象锁的函数。
B.synchronized修饰代码块
实例如下:

public enum Person {
    Alice,
    Bob;

    public synchronized void say() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " say hello world! " + TimeUtil.getCurrentTime());
    }

    public void speak() {
        synchronized (Alice) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " speak hello world! " + TimeUtil.getCurrentTime());
        }
    }
}

public class ThreadA implements Runnable {
    @Override
    public void run() {
        Person.Alice.say();
    }
}

public class ThreadB implements Runnable {
    @Override
    public void run() {
        Person.Alice.speak();
    }
}

public class Main {
    public static void main(String[] args) {
        Thread threadA = new Thread(new ThreadA());
        threadA.setName("threadA");
        Thread threadB = new Thread(new ThreadB());
        threadB.setName("threadB");
        threadA.start();
        System.out.println("A启动了 " + TimeUtil.getCurrentTime());
        threadB.start();
        System.out.println("B启动了 " + TimeUtil.getCurrentTime());
    }
}

执行输出:
A启动了 35:26
B启动了 35:26
threadA say hello world! 35:28
threadB speak hello world! 35:30
可以看出A和B出现了互斥,A调用的say()函数用
synchronized关键字修饰,所以此时A占用Alice锁,speak中的代码块用synchronized (Alice)修饰,说明同样需要Alice锁,因此互斥。
改动speak函数:

public void speak() {
    synchronized (Bob) {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " speak hello world! " + TimeUtil.getCurrentTime());
    }
}

将需求锁改为Bob,执行输出:
A启动了 38:36
B启动了 38:36
threadA say hello world! 38:38
threadB speak hello world! 38:38
可以看到互斥消失了。
继续更改:

    public void speak() {
        synchronized (this) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " speak hello world! " + TimeUtil.getCurrentTime());
        }
    }

这里的this指的是需要调用当前函数的对象锁,执行输出:
A启动了 40:45
B启动了 40:45
threadA say hello world! 40:47
threadB speak hello world! 40:49
互斥又出现了,因为又在竞争Alice锁
(2)分析类锁
每一个类都有唯一且同一时间只能被唯一线程持有的类锁。
实例如下:

public enum Person {
    Alice,
    Bob;

    public static synchronized void say() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " say hello world! " + TimeUtil.getCurrentTime());
    }

    public static synchronized void speak() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " speak hello world! " + TimeUtil.getCurrentTime());
    }
}

public class ThreadA implements Runnable {
    @Override
    public void run() {
        Person.say();
    }
}

public class ThreadB implements Runnable {
    @Override
    public void run() {
        Person.speak();
    }
}

public class Main {
    public static void main(String[] args) {
        Thread threadA = new Thread(new ThreadA());
        threadA.setName("threadA");
        Thread threadB = new Thread(new ThreadB());
        threadB.setName("threadB");
        threadA.start();
        System.out.println("A启动了 " + TimeUtil.getCurrentTime());
        threadB.start();
        System.out.println("B启动了 " + TimeUtil.getCurrentTime());
    }
}

执行输出:
A启动了 44:18
B启动了 44:18
threadA say hello world! 44:20
threadB speak hello world! 44:22
明显看到线程互斥。
改动代码:

    public static void speak() {
        synchronized (Person.class) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " speak hello world! " + TimeUtil.getCurrentTime());
        }
    }

改动speak函数,将里面的的代码用 synchronized (Person.class) 修饰,执行输出:
A启动了 44:18
B启动了 44:18
threadA say hello world! 44:20
threadB speak hello world! 44:22
两个线程依旧互斥,因为还在竞争类锁。

总结:类锁类似对象锁,唯一且同一时间只能由唯一线程持有。

最后补充一点:类锁和对象锁是两套互斥机制,互不影响,具体看你的函数需求的是对象锁(哪个对象)还是类锁

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页