多线程之synchronized 关键字的用法

初识 synchronized 关键字

synchronized 提供了一种排他机制,也就是在同一时间只能有一个线程执行某些操作。也就是说如果多个线程对index变量(共享变量/资源同时操作)而引起的数据不一致的问题,我们可以使用该关键字去解决。

什么是 synchronized?

synchronized 关键字可以实现一个简单的策略来防止线程干扰 和内存一致性错误,如果一个对象对多个线程是可见的,那么对该对象的所有读或者写都将通过同步的方式来进行。

synchronized 关键字的用法

synchronized 可以用于对代码块或方法进行修饰,而不能够用于对 class 以及变量 进行修饰。
同步方法
就是在方法前面加上synchronized 关键字。

public synchronized void sync() { ... }
public synchronized static void staticSync() { ... }

同步代码块

private final Object MUTEX = new Object(); 
public void sync() { synchronized (MUTEX) { ... } }

举个例子:模拟大厅叫号,如果不使用synchronized关键字来保证数据的一致性,那么该程序就会出现数据不一致性,比如号的重复和超过最大值。

/**
 * 具体策略类 叫号机
 */
public class CounterWindowRunnable03 implements Runnable {

    // 最多受理 500 笔业务
    private static final int MAX = 500;

    // 起始号码,不做 static 修饰
    private int index = 1;

    private static final Object MONITOR = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (MONITOR) {
                if (index > MAX)
                    break;
                try {
                    TimeUnit.MILLISECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.format("请【%d】号到【%s】办理业务\n", index++, Thread.currentThread().getName());
            }
        }
    }

    public static void main(String[] args) {
        final CounterWindowRunnable03 task = new CounterWindowRunnable03();
        new Thread(task, "一号窗口").start();
        new Thread(task, "二号窗口").start();
        new Thread(task, "三号窗口").start();
        new Thread(task, "四号窗口").start();
    }

}

而加了synchronized 关键字之后,程序不会出现数据不一致的情况。

深入 synchronized 关键字

线程堆栈分析

synchronized 关键字提供了一种互斥机制,也就是说在同一时刻,只能有一个线程访问同步资源。下面看代码:

public class Mutex {

    private static final String MUTEX = new String();

    private void accessResource() {
        synchronized (MUTEX) {
            try {
                TimeUnit.MINUTES.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        final Mutex mutex = new Mutex();
        for (int i = 0; i < 5; i++) {
            new Thread(mutex::accessResource).start();
        }
    }

}

上面的代码中定义了一个方法 accessResource,并且使用同步代码块的方式对 accessResource 进行了同步,同时定义了5个线程调用 accessResource 方法,由于同步代码块的互斥性,只能有一个线程获取了mutex monitor 的锁,其他线程只能进入阻塞状态,等待获取 mutex monitor 锁的线程对其进行释放,运行上面的程序然后打开 JConsole 工具监控,如下图所示。
在这里插入图片描述
选中我画出来的本地进程,然后点击【连接】按钮进入 JConsole 控制台,将 tab 切换至【线程】,如上图所示。
随便选中程序中创建的某个线程,会发现只有个线程在 IMED WAITING(sleeping)状 态,其他线程都进人了 BLOCKED 状态,如下图所示。
在这里插入图片描述
使用 jstack 命令打印进程的线程堆栈信息
Thread-0 持有 monitor <0x00000000d7f8a910>的锁并且处于休眠状态中,那么其他线程将会无法进入 accessResource 方法。
在这里插入图片描述
Thread-1 线程进入 BLOCKED 状态并且等待着获取 monitor <0x00000000d7f8a910> 的锁,其他的几个线程同样也是 BLOCKED状态。

使用 synchronized 需要注意的问题

与 monitor 关联的对象不能为空

private static final Object MUTEX = null; 
private void accessResource() { 
	synchronized (MUTEX) { 
		try {
			TimeUnit.MINUTES.sleep(10); 
		} catch (InterruptedException e) { 
			e.printStackTrace(); 
		} 
	} 
}

Mutex 为 null,很多人还是会犯这么简单的错误,每一个对象和一个 monitor 关联, 对象都为 nul l了,monitor肯定就不存在了。
synchronized 作用域太大
由于 synchronized 关键字存在排他性,也就是说所有的线程必须串行地经过 synchronized 保护的共享区域,如果 synchronized 作用域越大,则代表着其效率越低, 甚至还会丧失并发的优势。

@Override 
public synchronized void run() {
 	// 
 }

上面的代码对整个线程的执行逻辑单元都进行了 synchronized 同步,从而丧失了并发的能力,synchronized 关键字应该尽可能地只作用于共享资源(数据)的读写作用域。
不同的 monitor 企图锁相同的方法

public class Task implements Runnable { 
	private final Object MUTEX = new Object(); 
	@Override 
	public void run() { 
		synchronized (MUTEX) { } 
	}
	public static void main(String[] args) { 
		for (int i = 0; i < 5; i++) { 
			new Thread(Task::new).start(); 
		} 
	}
 }

上面的代码构造了五个线程,同时也构造了五个 Runnable 实例,Runnable 作为线程逻辑执行单元传递给Thread。但是,synchronized 根本互斥不了与之对应 的作用域,线程之间进行 monitor lock 的争抢只能发生在与 monitor 关联的同一个引用上,上面的代码每一个线程争抢的 monitor 关联引用都是彼此独立的,因此不可能起到互斥的作用。
多个锁的交叉导致死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成 的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系 统产生了死锁,这些永远在互相等待的进程称为死锁进程。死锁代码如下:
AService:

public class AService {
    private BService bService;
    private final Object LOCK = new Object();
    public void m1() {
        synchronized (LOCK) {
            System.out.println("AService m1");
            bService.s1();
        }
    }
    public void m2() {
        synchronized (LOCK) {
            System.out.println("AService m2");
        }
    }
    public void setbService(BService bService) {
        this.bService = bService;
    }
}

BService:

public class BService {
    private AService aService;
    private final Object LOCK = new Object();
    public void s1() {
        synchronized (LOCK) {
            System.out.println("BService s1");
        }
    }
    public void s2() {
        synchronized (LOCK) {
            System.out.println("BService s2");
            aService.m2();
        }
    }
    public void setaService(AService aService) {
        this.aService = aService;
    }
}

DeadLockTest:

public class DeadLockTest {

    public static void main(String[] args) {
        AService aService = new AService();
        BService bService = new BService();

        aService.setbService(bService);
        bService.setaService(aService);

        new Thread(() -> {
            while (true)
                aService.m1();
        }).start();

        new Thread(() -> {
            while (true)
                bService.s2();
        }).start();
    }

}

该程序就会产生死锁。

总结:

好了,本次分享就到这里,下一节,我会给大家详细讲解一下锁对象。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值