Java 线程安全

I. 线程安全

在使用线程时,如果每个线程所执行的任务中,涉及的变量仅仅是线程内部变量或该变量仅有该线程读写,那么此线程是安全的.
但如果多个线程同时读写同一个变量的话,发生的状况往往是变量最后的值和预期不同. 这是因为多个线程在同时执行时,每个线程都会对这个变量进行操作.
这里可以用一个生活中的例子来说明:在订火车票时,某班列车只剩下最后一张票了,但有两人刚好同时看到并预定了这张票,同时因为服务器采用的是多线程,每个用户独享一个单独的线程,那么这两个人将会同时订到这张票,但这显然是不符合生活经验的.
下面的例子便说明了这一点.

public class UnsafeThread implements Runnable {
    public int count = 0;

    @Override
    public void run() {

        try {
            TimeUnit.MILLISECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        count++;
    }

    public int getCount() {
        return count;
    }

    public static void main(String [] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        UnsafeThread thread = new UnsafeThread();
        for (int i = 0; i < 1000; i++) {
            executor.execute(thread);
        }
        executor.shutdown();

        System.out.println("count ==> " + thread.getCount());

    }
}

output://
count ==> 926

在不使用线程的情况下,count 的值应为1000;但因为多个线程同时对count 进行操作,最后的结果只有926.
在顺序编程中,对一个变量执行两次++,在不考虑特殊情况下得到的值一定是2;但如果是用多线程进行操作:线程A 增加了count一次,现在count 等于2;而同时线程B 的任务也是增加count 的值,但线程B 不知道线程A 增加了这个count 值,所以在线程B 执行任务的时候,它得到的count 值为1.
通过上述例子,可以看出,因为线程自身的特殊性,在使用并发时多个线程,如果没有特殊的机制来确保变量的正常操作,那么线程将不会被广泛采用.

II. Synchronised 关键字

对方法使用synchronised 关键字,可以避免上述的情况.
这里可以用排队的例子来说明. 在原来的情况中,线程们就像是没有排队的顾客一样,全都挤向了前台;而收银员,也就是被操作的变量,因为顾客太多而被弄得昏头转向,不免出了差错. 现在加上了syncrhonised,相当于在前台加上了栏杆(摆放在前台用来规范队列的东西是什么…),强制顾客们整齐地排成了一条队伍,一次只有一个顾客点餐.

public class SynchronizedThread implements Runnable {
    private int count = 0;

    @Override
    public synchronized void run() {
        count++;
    }

    public int getCount() {
        return count;
    }

    public static void main(String [] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        SynchronizedThread thread = new SynchronizedThread();
        for (int i = 0; i < 1000; i++) {
            executor.execute(thread);
        }
        executor.shutdown();

        System.out.println("count ==> " + thread.getCount());

    }

}

output://
count ==> 1000

run()方法前加上了synchronised,这样在访问count 变量时,一次只有一个而不是多个线程了.


同时要注意的是,被访问的变量一定要设置为private,不然尽管通过方法操作变量的线程被规范了,直接访问变量的线程却可以任意修改.

III. Lock

Lock 可以理解为显示地使用synchronised 机制.
尽管Lock 在语法上没有synchronised 优雅,但在执行一些复杂操作,那么Lock 将具有synchronised 所不具备的灵活性.

public class LockedThread implements Runnable {
    private int count = 0;
    private final ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        boolean captured = lock.tryLock();

        try {
            count++;
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (captured) lock.unlock();
        }
    }

    public int getCount() {
        return count;
    }

    public static void main(String [] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        SynchronizedThread thread = new SynchronizedThread();
        for (int i = 0; i < 1000; i++) {
            executor.execute(thread);
        }
        executor.shutdown();

        System.out.println("count ==> " + thread.getCount());

    }

}

output://
count ==> 1000

获得lock 需要调用方法tryLock(),同时在任务执行完后,需要调用unlock()来释放锁. 所以推荐方法是在获取锁后,将执行代码放入一个try 模块中,并在finally 模块中释放锁.

IV. Volatile 关键字

原子性(atomic)是除了使用synchronised 关键字和Lock 方法外另一个确保线程安全的方法,它指的是任务操作不可中断性. 但是Java 中的操作大多不是原子性的,并且使用原子性的尝试往往会失败. 因此,不要轻易尝试原子性,除非你已是并发专家.
volatile 关键字便是实现原子性的方法之一. 将一个变量设为volatile 后,每一个线程在改变该变量时,所有的线程都可以看到这个改变.
实例为将本章最开始的例子中的count 加上volatile 关键字.

public class UnsafeThread implements Runnable {
    private volatile int count = 0;

    @Override
    public void run() {

        try {
            TimeUnit.MILLISECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        count++;
    }

    public int getCount() {
        return count;
    }

    public static void main(String [] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        UnsafeThread thread = new UnsafeThread();
        for (int i = 0; i < 1000; i++) {
            executor.execute(thread);
        }
        executor.shutdown();

        System.out.println("count ==> " + thread.getCount());

    }
}

output:// 
count ==> 767

此段解释可能稍有错误
但是从输出可以看出,即使加上了volatile 关键字,count 仍未到达1000. 这和JVM 的运行机制有关. 每个线程在运行时,其实是独自拥有着一个线程栈,并从总栈中复制所需要的变量;而volatile的作用则是强制线程在改变变量后,将这个新的变量推送到总栈中. 但由于这种推送的不及时性,所以导致count的结果依旧不是我们想要的结果.
这个例子也侧面说明了,Java 自身不支持原子性,因此如果除非你是并发专家,不然不要依赖于原子性.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值