(二)synchronized详解
1、了解synchronized
synchronized是Java中的关键字,是一种同步锁。当多个并发线程访问同一个对象中用synchronized修饰的代码块时,在同一时刻只能有一个线程得到执行,其他的线程均受阻塞,必须等待当前线程执行完毕,其他线程才能执行该代码块。
当前执行线程和其他线程是互斥的,因为在执行synchronized代码块时会锁定当前的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象。
2、使用synchronized修饰方法
实现线程有序计数
class Num implements Runnable {
private int count;
public Num() {
count = 0;
}
public synchronized void run() {//获取锁并排斥其他线程
/*
* 使用synchronized修饰run()方法
* 被修饰的方法称为同步方法
* 其作用的范围是整个方法
* 作用的对象是调用这个方法的对象
*/
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "数了" + (++count));
}
}//释放锁
}
public class Demo1 {
public static void main(String[] args) {
Num n = new Num();
/*
* 使用Thread(Runnable target, String name) 这种构造方法
* 参数name就是新线程名称
*/
Thread thread1 = new Thread(n, "Tom");
Thread thread2 = new Thread(n, "Mike");
thread1.start();
thread2.start();
}
}
使用synchronized修饰的结果如下:
当一个线程在执行任务代码时,另一个线程是被阻塞的,因此只有在Mike计数完成之后,Tom才开始计数。
对之前的代码稍作修改加以对比:
public class Demo1 {
public static void main(String[] args) {
Thread thread1 = new Thread(new Num(), "Tom");
Thread thread2 = new Thread(new Num(), "Mike");
thread1.start();
thread2.start();
}
}
此时结果如下:
之所以出现使用了synchronized修饰,仍然乱序的结果,是因为因为synchronized只锁定对象,每个对象只有一个锁(lock)与之相关联,修改代码后相当于每个线程各自又创建了一个对象,因此存在两个对象以及两把锁,所以出现乱序,这也就意味着如果要使用同步锁,必须保证至少有两个或以上的线程,同时这多个线程都使用同一把锁。
注意:
- synchronized关键字不能继承。
虽然可以使用synchronized来修饰方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中重写了这个方法,在子类中的这个方法默认情况下并不是同步的,必须显式地在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。这两种方式的例子代码如下:
//在子类方法中添加synchronized关键字
class Parent {
public synchronized void fun() {
}
}
class Child extends Parent {
public synchronized void fun() {
}
}
//在子类方法中调用父类的同步方法
class Parent {
public synchronized void fun() {
}
}
class Child extends Parent {
public void fun() {
super.fun();
}
}
- 在创建接口中的方法时不能使用synchronized关键字。
- 构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步。
3、使用synchronized修饰静态方法
静态同步方法在进内存时不存在对象,但是存在其所属类的class类型的字节码文件对象,因此静态同步方法的锁就是该对象(.class),锁定的是所属类的所有对象。
对 “ 实现线程有序计数 ” 案例做以下修改
class Num implements Runnable {
// 静态方法需要调用静态变量
private static int count;
public Num() {
count = 0;
}
// 定义静态同步方法
public synchronized static void fun() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "数了" + (++count));
}
}
// 重写run()方法,调用静态同步方法
public void run() {
fun();
}
}
public class Demo2 {
public static void main(String[] args) {
Thread thread1 = new Thread(new Num(), "Tom");
Thread thread2 = new Thread(new Num(), "Mike");
thread1.start();
thread2.start();
}
}
此时结果如下:
虽然2个线程在执行时分别创建了2个对象,但由于run()方法调用了静态同步方法fun(),静态方法是属于类的,所以这2个对象相当于用了同一把锁,即所属类的字节码文件。虽然结果与Demo1相同,但实现原理是不同的。
4、使用synchronized修饰代码块
使用synchronized修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象,例如依然修改上述代码,将修饰方法改为修饰代码块:
class Num implements Runnable {
private static int count;
public Num() {
count = 0;
}
public void run() {
synchronized (this) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "数了" + (++count));
}
}
}
public int getCount() {
return count;
}
}
运行结果与修饰方法是相同的,只是形式不同,这里在方法内修饰代码块的作用域与直接修饰方法是一样的,都是run()方法内部。
当一个程序内存在使用synchronized修饰的代码块以及普通代码块时,多个线程可以同时访问这些代码块,访问普通代码块的线程之间仍然是争抢CPU的状态,访问同步代码块的线程受同步锁的影响会在结果上呈现先后顺序,示例代码如下:
class Test implements Runnable {
/*
* 创建测试类Test继承Runnable接口
* 该类包含两个方法 fun1()方法是使用synchronized修饰的
* 由线程A、C执行
* fun2()方法是普通方法
* 由线程B、D执行
* 重写run()方法
* 使4个线程分别能够执行fun1()和fun2()
*/
private int count;
public Test() {
count = 0;
}
public void fun1() {
synchronized (this) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
}
}
}
public void fun2() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "抢到了CPU!");
}
}
public void run() {
String threadName = Thread.currentThread().getName();
if (threadName.equals("A")) {
fun1();
} else if (threadName.equals("B")) {
fun2();
} else if (threadName.equals("C")) {
fun1();
} else if (threadName.equals("D")) {
fun2();
}
}
}
public class Demo3 {
public static void main(String arg[]) {
Test t = new Test();
Thread thread1 = new Thread(t, "A");
Thread thread2 = new Thread(t, "B");
Thread thread3 = new Thread(t, "C");
Thread thread4 = new Thread(t, "D");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
此时结果如下:
线程B、D执行普通代码块,始终在争抢CPU,线程A、C执行同步代码块,因此是线程A执行完任务代码后,线程C才开始执行;但要注意线程A在执行时也同线程B、D在争抢CPU,这也证明了程序内存在同步代码块以及普通代码块的时候,线程是可以同时访问这些代码块并且互相之间不排斥的。
5、修改同步锁
对 Demo3 案例做一下修改
class Test implements Runnable {
private int count;
Object obj = new Object();
public Test() {
count = 0;
}
public void fun1() {
synchronized (obj) {// 该锁是obj对象
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
}
}
}
public synchronized void fun2() {// 该锁是this对象
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + "抢到了CPU!");
}
}
public void run() {
String threadName = Thread.currentThread().getName();
if (threadName.equals("A")) {
fun1();
} else if (threadName.equals("B")) {
fun2();
}
}
}
public class Demo4 {
public static void main(String arg[]) {
Test t = new Test();
Thread thread1 = new Thread(t, "A");
Thread thread2 = new Thread(t, "B");
thread1.start();
thread2.start();
}
}
此时结果如下:
虽然2个线程传入的是同一个对象t,但在调用方法是,同步代码块的锁是obj对象,同步方法的锁是this对象,因此呈现在结果中,线程A、B依然在争抢CPU。
对上述代码加以修改:
class Test implements Runnable {
private int count;
public Test() {
count = 0;
}
public void fun1() {
synchronized (this) {// 该锁改为this对象
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
}
}
}
public synchronized void fun2() {// 该锁依然是this对象
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + "抢到了CPU!");
}
}
public void run() {
String threadName = Thread.currentThread().getName();
if (threadName.equals("A")) {
fun1();
} else if (threadName.equals("B")) {
fun2();
}
}
}
此时结果如下:
当两个线程调用的方法的锁都是this对象时,线程A、B受到同步锁的影响,只有一个线程执行完任务代码之后,另一个线程才开始执行。
提示:
当有一个明确的对象作为锁时,就采用以下类似的代码:
public void fun(){
// obj 锁定的对象
synchronized(obj){
// 要完成的任务
}
}
当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的对象来充当锁:
class Test implements Runnable
{
Object obj = new Object(); // 特殊的对象
public void fun() {
synchronized(obj) {
// 要完成的任务
}
}
}
6、单例设计模式中懒汉式并发访问的安全问题
有关单例设计模式的内容,请看单例设计模式。
class Single {
private Single() {
}
private static Single s;
public static Single getInstance() {
if (s == null) {
/*
* 之所以说懒汉式并发访问存在安全问题
* 原因就在这里
* 假设当线程t1抢占到CPU
* 执行到该注释位置时,CPU被t2抢走
* 当t2执行到该注释位置时
* CPU再次被t1抢回来
* 那么此时t1已经判断过s为空
* 会直接执行下一句代码创建对象
* 当t2也抢到CPU
* 也已经判断过s为空
* 同样会直接执行下一句代码创建对象
* 那么此时就会创建2个对象
* 无法保证单例
*/
s = new Single();
}
return s;
}
}
class Test implements Runnable {
public void run(){
Single s = Single.getInstance();
}
}
public class Demo5 {
public static void main(String[] args) {
Test t = new Test();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
t2.start();
}
}
避免懒汉式出现该问题的方法之一,就是结合使用synchronized代码块,修改代码如下:
class Single {
private Single() {
}
private static Single s;
public static Single getInstance() {
/*
* 使用synchronized代码块包围创建对象部分
* 此时的锁是Single.class
* 由于判断锁需要消耗更多性能
* 因此添加if判断
* 可保证线程过多时
* 从第三个线程开始
* 先判断对象是否存在
* 而不是直接判断锁
* 从而提高性能
*/
if (s == null) {
synchronized (Single.class) {
if (s == null) {
s = new Single();
}
}
}
return s;
}
}
7、总结
-
当synchronized关键字作用的对象是非静态的,那么它取得的锁是对象;当synchronized作用的对象是静态的,那么它取得的锁是该类的字节码文件,该类的所有对象用同一把锁。
-
每个对象只有一个锁(lock)与之相关联,哪个线程获得这个锁,该线程就可以运行它所控制的那段代码,此时其他线程无法访问。
-
实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
注意:
随着JKD版本的更新,在1.5版本之后出现比synchronized更加强大的实现同步锁的方法,详情参考使用Lock接口与Condition接口实现生产者与消费者。
版权声明:欢迎转载,欢迎扩散,但转载时请标明作者以及原文出处,谢谢合作! ↓↓↓