——Java培训、Android培训、iOS培训、.Net培训、期待与您交流! ——-
一、同步
在java中对于多线程的安全问题提供了专业的解决方式——synchronized(同步)。
对多条操作共享数据的语句,只能让一个线程都执行完。在执行过程中,其他线程不能参与执行。
需要同步的前提:必须要有两个或者两个以上的线程;必须是多个线程使用同一个锁;必须要保证同步中只有一次线程执行。
同步方式一共有两种:同步代码块和同步函数
- 同步函数: 即有synchronized关键字修饰的方法。
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
public class SynchronizedCounter {
private int c = 0;
public synchronized void increment() {
c++;
}
public synchronized void decrement() {
c--;
}
public synchronized int value() {
return c;
}
}
注意:
函数都有自己所属的对象this,所以同步函数所使用的锁就是this锁。
构造函数不能为同步的——在构造函数前使用synchronized关键字将导致语义错误。同步构造函数是没有意义的。这是因为只有创建该对象的线程才能调用其构造函数。
- 同步代码块:
即有synchronized关键字修饰的语句块。
被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。
下面是一个实例:
class Ticket implements Runnable
{
private int tick=100;
Object obj = new Object();
public void run()
{
while(true)
{
//给程序加同步,即锁
synchronized(obj)
{
if(tick>0)
{
try
{
//使用线程中的sleep方法,模拟线程出现的安全问题
Thread.sleep(10);
}
catch (Exception e)
{
}
System.out.println(Thread.currentThread().getName()+"..tick="+tick--);
}
}
}
}
}
注意:上个代码里面同步代码块使用的锁可以是任意对象。
每一个对象都有一个与之相关联动的内部锁。按照传统的做法,当一个线程需要对一个对象的字段进行排他性访问并保持访问的一致性时,他必须在访问前先获取该对象的内部锁,然后才能访问之,最后释放该内部锁。在线程获取对象的内部锁到释放对象的内部锁的这段时间,我们说该线程拥有该对象的内部锁。只要有一个线程已经拥有了一个内部锁,其他线程就不能在拥有该锁了。其他线程将会在试图获取该锁的时候被阻塞了。
- 静态函数的同步方式
当同步函数被static修饰时,这时的同步用的是哪个锁呢?
静态函数在加载时所属于类,这时有可能还没有该类产生的对象,但是该类的字节码文件加载进内存就已经被封装成了对象,这个对象就是该类的字节码文件对象。
所以静态加载时,只有一个对象存在,那么静态同步函数就使用的这个对象。
这个对象就是 类名.class
最经典的例子便是懒汉式加同步 ,代码如下:
class Single{
private static Single s = null;
private Single(){}
public static Single getInstance(){
if(s == null){
synchronized(Single.class){
if(s == null)
s = new Single();
}
}
return s;
}
}
这样的做法,既保证了线程安全,又可以直接调用静态的对象。
二、死锁
死锁对Java程序来说,是很复杂的,也很难发现问题。当两个线程被阻塞,每个线程在等待另一个线程时就发生死锁。
当同步中嵌套同步时,就有可能出现死锁现象。下面就是个例子:
public class DeadlockRisk {
private static class Resource {
public int value;
}
private Resource resourceA = new Resource();
private Resource resourceB = new Resource();
public int read() {
synchronized (resourceA) {
synchronized (resourceB) {
return resourceB.value + resourceA.value;
}
}
}
public void write(int a, int b) {
synchronized (resourceB) {
synchronized (resourceA) {
resourceA.value = a;
resourceB.value = b;
}
}
}
}
当有2个线程,1个线程在执行read()里synchronized (resourceA)时,另一个线程在执行write()里synchronized (resourceB),此时就会发生死锁,程序就死掉。有一些设计方法能帮助避免死锁,包括始终按照预定义的顺序获取锁这一策略。
三、线程通讯
线程的通讯就是运用(上一篇中)线程调度方法,是多个线程在操作同一个资源,但是操作的动作不同。下面是个实例:
public class ThreadA {
public static void main(String[] args) {
ThreadB b = new ThreadB();
//启动计算线程
b.start();
//线程A拥有b对象上的锁。线程为了调用wait()或notify()方法,该线程必须是那个对象锁的拥有者
synchronized (b) {
try {
System.out.println("等待对象b完成计算。。。");
//当前线程A等待
b.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("b对象计算的总和是:" + b.total);
}
}
}public class ThreadB extends Thread {
int total;
public void run() {
synchronized (this) {
for (int i = 0; i < 101; i++) {
total += i;
}
notify();//线程A被唤醒
}
}
}
要注意的是:对象上调用wait()方法时,执行该代码的线程会立即放弃它在对象上的锁。然而调用notify()时,并不这时线程会放弃其锁。如果线程仍然在完成同步代码,则线程不会放弃锁直到完成后。
四、Lock接口
在JDK1.5中,专门提供了锁对象,利用锁可以方便的实现资源的封锁,这些内容主要集中在java.util.concurrent.locks包下面,里面有三个重要的接口Condition、Lock、ReadWriteLock。
Condition:Condition将Object监视器方法(wait、notify和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意Lock实现组合使用,为每个对象提供多个等待 set(wait-set)。
Lock:Lock实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作。
ReadWriteLock:ReadWriteLock维护了一对相关的锁定,一个用于只读操作,另一个用于写入操作。
下面是一个使用Lock接口的同步方法:
思路:设有一个绑定的缓冲区,它支持 put 和 take 方法。如果试图在空的缓冲区上执行 take 操作,则在某一个项变得可用之前,线程将一直阻塞;如果试图在满的缓冲区上执行 put 操作,则在有空间变得可用之前,线程将一直阻塞。我们喜欢在单独的等待 set 中保存put 线程和 take 线程,这样就可以在缓冲区中的项或空间变得可用时利用最佳规划,一次只通知一个线程。可以使用两个Condition 实例来做到这一点。
代码如下:
class BoundedBuffer {
final Lock lock = new ReentrantLock();//创建一个锁对象
final Condition notFull = lock.newCondition(); //缓冲池信号量
final Condition notEmpty = lock.newCondition(); //缓冲池信号量
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock(); //互斥信号量的锁
try {
while (count == items.length) //缓冲池满会进入等待状态
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal(); //释放一个信号量
}
finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
}
finally {
lock.unlock();
}
}
}
使用上面的接口方法同步,和锁的方法一样。所以同步是隐示的锁操作,而Lock对象是显示的锁操作,它的出现就替代了同步。
五、总结
多线程的目的是为了最大限度的利用CPU资源。但是多线程的创建和停止会导致线程不安全情况。
因此通过线程的加锁或者使用同步代码块来进行。这两种操作实际都是对线程,通过notify()、notifyAll() 、wait() 方法来进行调度。
在JDK1.5更是用了专门的接口来实施上面的方法,使线程调度更加方便和安全。