并发在任何系统和编程语言中都有着重要的地位。
操作系统中的互斥和同步
在操作系统(假设单核)中,我们可以实现同时多个进程(软件)的同时运行,其实取决于操作系统的中断,也就是一个进程在cpu上执行一个时间片后就会被中断,然后换上其它的进程上来执行,所以我们的感觉是进程都在并发的执行。或许你会问,为什么不一直执行,切换过去切换过来不消耗资源的嘛?对的,进程的切换时耗费资源,但是你得注意一种情况,就是很多时候你的进程其实并没有消耗cpu,也就是它可能正在io阻塞中,这样,cpu的切换的执行就会更加的高效,特别是那些io密集型的进程。
然而,我们在操作系统中,各个进程可能会访问同一个资源(文件)(当然,直觉上这也是不可以的),所以必须想办法去让两个或者多个进程不能同时对一个相同的资源去使用,最典型的就是不能同时去写一个文本文件。当然这里我们必须想办法让他们不能同时使用(互斥),所以操作系统的典型处理就是在内核上个屏蔽中断
和TSL(Test and Set Lock)
,软件层次的信号量和PV操作
。通过这些方式,我们基本上就能够完成操作系统的互斥和同步了。
java中的互斥和同步
但是在面向编程语言的,还有一种对互斥和同步的实现,那就是管程。
管程 (英语:Monitors,也称为监视器) 是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。这些共享资源一般是硬件设备或一群变量。管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。与那些通过修改数据结构实现互斥访问的并发程序设计相比,管程实现很大程度上简化了程序设计。
对,java就是实现了管程的。
互斥 - synchronized 和 lock
java中通过了关键字synchronized和java.util.concurrent.locks来提供互斥性。
- synchronized
- synchronized关键字修饰的代码块称为同步代码块。
- 对于每个对象来说,synchronized方法共享一个锁,也就是同时只有一个线程能够访问被synchronized标示的所有方法。
- 类锁和对象锁,类锁指被
static
修饰的方法,对象就是new
出来的那个,其中类锁是在class
上的锁,所以所有调用的都共享同一个锁,而对象锁则不同的对象有不同的锁。
- **java.util.concurrent.locks **
- 显式的加锁,常见的
ReentrantLock
。 - 使用显式的锁,你能够更好的控制锁的细节,及何时获取释放等。
- 在获取
ReentrantLock
的同时可以interrupt()
,及在获取锁的同时可以被中断。(当然在获取synchronized的时候不能够使用interrupt()
中断)。
同步 - wait 和 notify
在说同步前,先讲一讲中断interrupt
。
在java中,interrupt()
可以中断线程,然后抛出InterruptException
。当然这中间也有他们的游戏规则,也就是当一个线程调用了interrupt()
后,如果线程处于wait()
或者sleep()
中就会立即抛出异常,否则就会等他们进入阻塞的调用后再抛出。其实这个也好理解,你不能强制立即让它停止吧,当它还在活动的时候至少也得等它把手上的事做完吧。
java中通过了关键字wait和notify来提供同步性。
- wait
- wait 是释放已经获取的锁,所以调用它的时候必须已经获取锁。及在synchronized锁修饰的方法中的调用。
- 和
sleep
和yield
的对比,最重要的是wait
释放了锁,而sleep
则只是把线程挂起。
- notify,notifyAll
- 调用前自己得获取了锁
- 调用
notify
会唤醒一个wait
过的线程,notifyAll
则会唤醒所有被wait
的线程。 -
notifyAll
被调用的时候,只会唤醒那些等待相应的锁的任务才会被唤醒,而不是ALL。