多线程安全问题,是由于多个线程在访问共享的数据(共享的资源),并且操作共享数据的语句不止一条。那么这样在多条操作共享数据的之间线程就可能发生切换,从而引发线程安全问题。
例如如下情况:
public class ThreadDemo {
public static void main(String[] args){
Apple apple = new Apple();
Thread t1 = new Thread(apple, "小明");
Thread t2 = new Thread(apple, "二明");
Thread t3 = new Thread(apple, "大明");
Thread t4 = new Thread(apple, "小兰");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class Apple implements Runnable{
/*有6个苹果用于分配*/
int num = 6;
@Override
public void run() {
while (true){
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "拿了第" + num-- + "个苹果");
}else {
break;
}
}
}
}
运行结果:
小兰拿了第5个苹果
小明拿了第6个苹果
大明拿了第4个苹果
二明拿了第3个苹果
小明拿了第2个苹果
大明拿了第1个苹果
小兰拿了第0个苹果
二明拿了第-1个苹果
小明拿了第-2个苹果
这个问题中,为什么会出现有人拿到了0,-1,-2个苹果的情况呢?因为当二明进入run()方法时,还没来得及做num-- 操作时,此时num=1,另外两个线程也进入到run()方法,执行拿苹果的动作,所以就出现了出现-1,-2的结果。
要解决上述多线程并发访问临界资源出现的安全性问,就要保证sleep()和拿苹果的操作同步完成。也即是说,在第一个线程拿完苹果,进行num--之前,其他线程不能进行 num>0 的判。
想要知道你的多线程程序有没有安全问题,只要看线程任务中是否有多条代码在处理共享数据。
解决多线程并发访问资源的安全问题,有三种方式:同步代码块、同步函数、锁机制(Lock)。
方法一、同步代码块
语法:
synchronized(同步锁){
//需要同步操作的代码
}
同步锁:
为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制.也称为同步监听对象/同步锁/同步监听器/互斥锁。
实际上,对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁,谁拿到锁,谁就可以进入代码块,其他线程只能在代码块外面等着,而且注意,在任何时候,最多允许一个线程拥有同步锁.
Java程序运行可以使用任何对象作为同步监听对象,但是一般的,我们把当前并发访问的共同资源作为同步监听对象。
public class ThreadDemo {
public static void main(String[] args){
Apple apple = new Apple();
Thread t1 = new Thread(apple, "小明");
Thread t2 = new Thread(apple, "二明");
Thread t3 = new Thread(apple, "大明");
Thread t4 = new Thread(apple, "小兰");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class Apple implements Runnable{
/*有6个苹果用于分配*/
int num = 6;
// Object obj = new Object();
@Override
public void run() {
while (true){
synchronized (Apple.class){
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "拿了第" + num-- + "个苹果");
}else {
break;
}
}
}
}
}
运行结果:
小兰拿了第5个苹果
小明拿了第6个苹果
大明拿了第4个苹果
二明拿了第3个苹果
小明拿了第2个苹果
大明拿了第1个苹果
方法二、同步函数;
使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着.
Synchronized public void doFunction(){
//需要同步的内容
}
同步锁是谁:
对于非static方法,同步锁就是this.
对于static方法,我们使用当前方法所在类的字节码对象(xxx.class).
public class ThreadDemo {
public static void main(String[] args){
Apple apple = new Apple();
Thread t1 = new Thread(apple, "小明");
Thread t2 = new Thread(apple, "二明");
Thread t3 = new Thread(apple, "大明");
Thread t4 = new Thread(apple, "小兰");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class Apple implements Runnable{
/*有6个苹果用于分配*/
int num = 6;
// Object obj = new Object();
@Override
public void run() {
while (true){
doRun();
}
}
public synchronized void doRun(){
synchronized (Apple.class){
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "拿了一个苹果,还剩" + num-- + "个");
}
}
}
}
运行结果:
小明拿了一个苹果,还剩6个
小明拿了一个苹果,还剩5个
小明拿了一个苹果,还剩4个
小明拿了一个苹果,还剩3个
小明拿了一个苹果,还剩2个
小明拿了一个苹果,还剩1个
方法三、锁机(同步锁)
Lock锁代替了同步代码块。在多线程中能够使用同步代码块的地方都可以使用Lock接口来代替,在java.util.concurrent.locks包下有接口Lock它是专门放负责描述锁这个事务。
当我们使用Lock接口代替同步代码块的时候,就需要程序员手动的来获取锁和释放锁,如果在程序中出现了获取锁之后,没有释放锁,导致其他程序无法进入这个同步语句中。需要使用try-finally语句在finally中书写释放锁的代码。
在Lock接口中lock方法和unLock方法分别是获取锁和释放锁。
class AppleAllot implements Runnable{
int num = 6;
Lock lock = new ReentrantLock();
@Override
public void run() {
while (true){
//获取锁
lock.lock();
try{
if (num > 0){
System.out.println(Thread.currentThread().getName() + "拿了第" + num+ "个苹果");
num--;
}
}finally {
//释放锁
lock.unlock();
}
}
}
}
public class LockDemo{
public static void main(String[] args){
AppleAllot apple = new AppleAllot();
Thread t1 = new Thread(apple, "小明");
Thread t2 = new Thread(apple, "二明");
Thread t3 = new Thread(apple, "大明");
Thread t4 = new Thread(apple, "小兰");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
运行结果:
小明拿了第6个苹果
小明拿了第5个苹果
小明拿了第4个苹果
小明拿了第3个苹果
小明拿了第2个苹果
小明拿了第1个苹果
说明:
Synchronized:
使用synchronized关键字修饰方法,会导致加锁,虽然可以使该方法线程安全,但是会极大的降低该方法的执行效率,故要慎用该关键字。
它无法中断一个正在等候获得锁的线程;
也无法通过投票得到锁,如果不想等下去,也就没法得到锁;
同步还要求锁的释放只能在与获得锁所在的堆栈帧相同的堆栈帧中进行,多数情况下,这没问题(而且与异常处理交互得很好),但是,确实存在一些非块结构的锁定更合适的情况。
锁机制:
Java.util.concurrent.lock 中的Lock 框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。
ReentrantLock类实现了Lock ,它拥有与synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。
参考:http://blog.csdn.net/caidie_huang/article/details/52748973