多线程线程安全与synchronized关键字


线程安全问题

对于上次的小实例,是强调过不考虑线程安全问题的。

如果我们通过调用线程类的sleep()方法,模拟CUP突然暂停对程序A的执行转而执行线程B。这在实际中是有可能发生的。

class Shopping implements Runnable {

	private int num = 10;

	public void run() {
		while (true) {
			if (num > 0) {
				try {
					//模拟CUP暂停对某线程的执行转而执行另一线程
					Thread.sleep(10);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() 
				+ "抢血拼到了第" + num-- + "双鞋");
			} else
				break;
		}
	}
}

public class Test {
	public static void main(String[] args) {
		Shopping shopping = new Shopping();

		Thread t1 = new Thread(shopping, "线程1");
		Thread t2 = new Thread(shopping, "线程2");

		t1.start();
		t2.start();
	}
}

多运行几次,有时候会出现这样的打印结果:

线程1抢血拼到了第10双鞋
线程2抢血拼到了第9双鞋
线程2抢血拼到了第8双鞋
线程1抢血拼到了第7双鞋
线程2抢血拼到了第6双鞋
线程1抢血拼到了第5双鞋
线程1抢血拼到了第4双鞋
线程2抢血拼到了第3双鞋
线程1抢血拼到了第2双鞋
线程2抢血拼到了第1双鞋
线程2抢血拼到了第0双鞋

很明显,这里就出现了线程安全问题,因为是没有第0双鞋的。

出现问题的原因,可能是这样。
在这里插入图片描述
线程1首先获得资源,此时num等于1大于0,进入if语句,线程1暂停执行转而执行线程2去了。得到num还是等于1,大于0,进入if语句,同样,此时线程2被暂停转而执行线程1去了。

紧接着线程1一鼓作气,执行了num–,num变成了0,线程1结束。但是可怕的地方来了,线程2从睡眠中醒来,继续执行下面的语句就是num–,问题是此时num等于0啊,那么就产生了这样的结果“线程2抢血拼到了第0双鞋”。

其中num是两个线程的共享数据。想详细了解,参见这篇博文:线程共享数据与线程私有数据。

synchronize关键字

那么要从根本上解决这个问题,我们就必须保证多个线程在对共享数据进行操作时完全同步。也就是说,当线程1在写入时,线程2不仅不能写入而且不能读。因为在线程1写完之前,线程2读取的一定是一个过期数据。Java提供了一个重要的关键字synchronize来实现这个功能。

关键字synchronize的作用是对需要同步的代码加锁,使得每一次,只能有一个线程进入同步代码块,从而保证线程共享数据的安全。

synchronize有多种用法,这里做一个简单的整理:

  • 作用于代码块:对给定对象加锁,进入同步代码块前要获得给定对象的锁。
  • 作用于实例方法:相当于对当前实例加锁,进入方法前要获得当前实例的锁。
  • 作用于静态方法:相当于对当前类加锁,进入方法前要获得当前类的锁。

1.作用于代码块

下述代码,将关键字synchronize作用于代码块,因此,每次当线程进入被synchronize包裹的代码块时,就会要求请求obj实例的锁。如果当前有其他线程正持有这把锁,那么新到的线程就必须等待。这样,就保证每次只有一个线程能进入synchronize作用的代码块。

class Shopping implements Runnable {
	private int num = 10;
	static Shopping shop = new Shopping();

	public void run() {
		while (true) {
			synchronized (shop) {
				if (num > 0) {
					try {
						//模拟CUP暂停对某线程的执行转而执行另一线程
						Thread.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName() 
					+ "抢血拼到了第" + num-- + "双鞋");
				} else
					break;
			}
		}
	}
}

public class Test {
	public static void main(String[] args) {
		Shopping shopping = new Shopping();

		Thread t1 = new Thread(shopping, "线程1");
		Thread t2 = new Thread(shopping, "线程2");

		t1.start();
		t2.start();
	}
}

2.作用于实例方法

我们还可以使用第二种用法改写上述代码:

class Shopping implements Runnable {
	private int num = 10;
	//stop的作用是跳出循环
	boolean stop = false;

	public void run() {
		while (true) {
			if (stop)
				break;
			stop = shop(stop);
		}
	}
	//synchronize作用于实例方法
	public synchronized boolean shop(boolean stop) {
		if (num > 0) {
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName() 
			+ "抢血拼到了第" + num-- + "双鞋");
		} else
			stop = true;
		return stop;
	}
}

public class Test {
	public static void main(String[] args) {
		Shopping shopping = new Shopping();
		Thread t1 = new Thread(shopping, "线程1");
		Thread t2 = new Thread(shopping, "线程2");
		t1.start();
		t2.start();
	}
}

上述代码中,synchronize作用于一个实例方法。也就是说进入shop()方法前,线程必须获得当前对象实例的锁。在本例中就是对象shopping

3.作用于静态方法

如果要使用第三种用法,即作用于静态方法上,此时int类型的变量num也要是static类型,不然访问不到。

class Shopping implements Runnable {
	//静态变量
	private static int num = 10;
	boolean stop = false;

	public void run() {
		while (true) {
			if (stop)
				break;
			stop = shop(stop);
		}
	}

	//synchronize作用于静态方法
	public static synchronized boolean shop(boolean stop) {
		if (num > 0) {
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName() 
			+ "抢血拼到了第" + num-- + "双鞋");
		} else
			stop = true;
		return stop;
	}
}

public class Test {
	public static void main(String[] args) {
		//这种方式当然可以实现,暂且注释掉,重点看另一种方式。
		/*Shopping shopping = new Shopping();
		Thread t1 = new Thread(shopping, "线程1");
		Thread t2 = new Thread(shopping, "线程2");*/
		Shopping s1 = new Shopping();
		Shopping s2 = new Shopping();
		Thread t1 = new Thread(s1, "线程1");
		Thread t2 = new Thread(s2, "线程2");
		t1.start();
		t2.start();
	}
}

从两个技术点进行分析:

  1. 虽然上诉代码,new了两个Shopping类型的实例。但是int型的变量num是静态变量,所有Shopping类型的实例共享一份;
  2. 虽然两个线程指向了不同的Shopping实例。但是synchronize作用于静态方法时,相当于对当前类即Shopping类加锁,进入方法前要获得当前类的锁,s1、s2都是Shopping类的实例,同为Shopping类的锁,因此,线程间可以正确同步。

这两个技术点缺一不可,下面我们分三种情况讨论:

  1. int类型变量num不是静态变量。这种情况不详细讨论,有明显错误——静态方法不能访问非静态变量。
  2. 把shop()方法只用synchronize修饰不用static修饰。此时,两个线程的Shopping类型实例不是同一个对象。因此,线程t1会在进入同步方法前加锁s1对象,而线程t2也加锁自己的对象s2。
    即,这两个线程使用的是两把不同的锁。因此,线程安全时无法保证的。
    多运行几次,还是会有这样的情况出现:
线程1抢血拼到了第0双鞋
  1. 前两种情况的叠加,既不使用static修饰变量num,也不修饰shop()方法。此时的运行结果是这样:
线程1抢血拼到了第10双鞋
线程2抢血拼到了第10双鞋
线程2抢血拼到了第9双鞋
线程1抢血拼到了第9双鞋
线程1抢血拼到了第8双鞋
线程2抢血拼到了第8双鞋
线程2抢血拼到了第7双鞋
线程1抢血拼到了第7双鞋
线程2抢血拼到了第6双鞋
线程1抢血拼到了第6双鞋
线程1抢血拼到了第5双鞋
线程2抢血拼到了第5双鞋
线程2抢血拼到了第4双鞋
线程1抢血拼到了第4双鞋
线程2抢血拼到了第3双鞋
线程1抢血拼到了第3双鞋
线程1抢血拼到了第2双鞋
线程2抢血拼到了第2双鞋
线程2抢血拼到了第1双鞋
线程1抢血拼到了第1双鞋

完全是各玩各的。即此时在Java堆中有两个Shopping类型实例s1和s2,线程t1和线程t2在操作各自实例中的num。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值