线程不安全

线程不安全的问题分析:在小朋友抢气球的案例中模拟网络延迟来将问题暴露出来;示例代码如下:

public class ImplementsDemo {

public static void main(String []args) {

Balloon balloon = new Balloon();

new Thread(balloon, "小红").start();

new Thread(balloon, "小强").start();

new Thread(balloon, "小明").start();

}

}

// 气球

class Balloon extends Thread {

private int num = 50;

@Override

public void run() {

for (int i = 0; i < 50; i++) {

if (num > 0) {

try {

Thread.sleep(10);

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(Thread.currentThread().getName() + "抢到了" + (num--) + "号气球")

}

}

}

}

在线程中的run方法上不能使用throws来声明抛出异常,所以在run方法中调用有可能出现异常的代码时,只能使用try-catch将其捕获来处理。

原因是:子类覆盖父类方法时不能抛出新的异常,父类的run方法都没有抛出异常,子类就更加不能抛出异常了。详情可查看我的另一篇文章 「JAVA」运行时异常、编译时异常、自定义异常,通过案例实践转译和异常链

)

在上述案例中,通过引入Thread.sleep();来模拟网络延迟,该方法的作用是让当前线程进入睡眠状态10毫秒,此时其他线程就可以去抢占资源了,方法的参数是睡眠时间,以毫秒为单位。

通过观察运行结果,发现了问题:

在运行结果中,小红、小强两个小朋友都抢到了14号气球,也就是14号气球被抢到了2次。我们来梳理线程的运行过程来看看发生了什么:

小强和小红两个线程都拿到了14号气球,由于线程调度,小强获得了CPU时间片,打印出了抢到的气球,而小红则进入睡眠;小强在打印后对num做了减一操作,此时num为13;

小明线程开始运行,抢到了13号气球,并对num做了减一操作,此时num为12;

小红线程醒来,打印出抢到的14号气球;此时的num为12,减一后结果为11;

由于多个线程是并发操作,所以对num做判断时可能上一个线程还未对num减一,故都能通过(num > 0)的判断;

然后再来运行上述代码,得出如下的结果:

运行结果中出现了本不该出现的0和-1,因为按照正常逻辑,气球数量到1之后就不应该被打印和减一了。出现这样的结果是因为出现了以下的执行步骤:

小红、小强、小明都同时抢到了1号气球,由于线程调度,小强获取了cpu时间片,得以执行,而小明和小红则进入睡眠;小强打印出结果后,对num减一,此时num为0;

小明醒来,获得的num为0,然后小明将num打印出来,再对num减一,此时num为-1;

小红醒来,获得的num为-1,随后小红将num打印出来,再对num减一,此时怒木为-2;

由于多个线程是并发操作,所以对num做判断时可能上一个线程还未对num减一,故都能通过(num > 0)的判断;

解决方案:

在案例中的抢气球其实是两步操作:先抢到气球,再对气球总数减一;既然是两步操作,在并发中就完全有可能会被分开执行,且执行顺序无法得到控制;

想要解决上述的线程不安全的问题,就必须要将这两步操作作为一个原子操作,保证其同步运行;也就是当一个线程A进入操作的时候,其他线程只能在操作外等待,只有当线程A执行完毕,其他线程才能有机会进入操作。

原子操作:不能被分割的操作,必须保证其从一而终完全执行,要么都执行,要么都不执行。

为解决多线程并发访问同一个资源的安全性问题,Java 提供如下了几种不同的同步机制:

同步代码块;

同步方法;

Lock 锁机制;

同步代码块

同步代码块: 为了保证线程能够正常执行原子操作,Java 引入了线程同步机制,其语法如下:

synchronized (同步锁) {

// 需要同步操作的代码

... ...

}

上述中同步锁,又称同步监听对象、同步监听器、互斥锁,同步锁是一个抽象概念,可以理解为在对象上标记了一把锁;

Java 中可以使用任何对象作为同步监听对象,但在项目开发中,我们会把当前并发访问的共享资源对象作为同步监听对象,在任何时候,最多只能运行一个线程拥有同步锁。

卫生间的使用就是一个很好的例子,一个卫生间在一段时间内只能被一个人使用,当一个人进入卫生间后,卫生间会被上锁,其他只能等待;只有当使用卫生间的人使用完毕,开锁后才能被下一个人使用。

然后就可以使用同步代码块来改写抢气球案例,示例代码如下:

public class ImplementsDemo {

public static void main(String []args) {

Balloon balloon = new Balloon();

new Thread(balloon, "小红").start();

new Thread(balloon, "小强").start();

new Thread(balloon, "小明").start();

}

}

// 气球

class Balloon implements Runnable {

private int num = 500;

@Override

public void run() {

for (int i = 0; i < 500; i++) {

synchronized (this) {

if (num > 0) {

System.out.println(Thread.currentThread().getName() + "抢到了"

+ num + "号气球");

num--;

}

}

}

}

}

通过查看运行结果,线程同步的问题已经得到解决。

同步方法

同步方法: 使用synchronized修饰的方法称为同步方法,能够保证当一个线程进入该方法的时候,其他线程在方法外等待。比如:

public synchronized void doSomething() {

// 方法逻辑

}

PS:方法修饰符不分先后顺序。

使用同步方法来改写抢气球案例,代码如下:

public class ImplementsDemo {

public static void main(String []args) {

Balloon balloon = new Balloon();

new Thread(balloon, "小红").start();

new Thread(balloon, "小强").start();

new Thread(balloon, "小明").start();

}

}

// 气球

class Balloon implements Runnable {

private int num = 500;

@Override

public void run() {

for (int i = 0; i < 500; i++) {

grabBalloon();

}

}

// 抢气球

private synchronized void grabBalloon() {

if (num > 0) {

System.out.println(Thread.currentThread().getName() + "抢到了"

+ num + "号气球");

num--;

}

}

}

注意:不能使用synchronized修改线程类中的run方法,因为使用之后,就会出现一个线程执行完了所有功能,多个线程出现串行;原本是多行道,使用synchronized修改线程类中的run方法,多行道变成了单行道。

synchronized 的好与坏

好:synchronized 保证了并发访问时的同步操作,避免了线程的安全性问题。

坏:使用synchronized 的方法、代码块的性能会比不用要低一些。

StringBuilder和StringBuffer

StringBuilder和StringBuffer 区别就在于StringBuffer中的方法都使用了synchronized修饰,StringBuilder中的方法没有使用synchronized修饰;这也是StringBuilder性能比StringBuffer高的主要原因。

Vector和ArrayList

两者都有同样的方法,有同样的实现算法,唯一不同就是Vector中的方法使用了synchronized修饰,所以Vector的性能要比ArrayList低。

Hashtable和HashMap

两者都有同样的方法,有同样的实现算法,唯一不同就是Hashtable中的方法使用了synchronized修饰,所以Hashtable的性能要比HashMap低。

volatile关键字

volatile 关键字的作用在于:被volatile 关键字修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而可以确保多个线程能正确处理该变量。

需要注意的是,volatile关键字可能会屏蔽虚拟机中的一些必要的优化操作,所以运行效率不是很高,因此,没有特别的需要,不要使用;即便使用,也要避免大量使用。

单例模式

单例模式--饿汉模式

代码如下:

public class SlackerDemo {

private SlackerDemo() {}

private static SlackerDemo instance = null;

public static SlackerDemo getInstance() {

if (instance == null) {

instance = new SlackerDemo();

}

return instance;

}

}

单例模式--懒汉模式

代码如下:

public class SlackerDemo {

private SlackerDemo() {}

private static SlackerDemo instance = null;

public static SlackerDemo getInstance() {

if (instance == null) {

instance = new SlackerDemo();

}

return instance;

}

}

懒汉模式存在线程不安全问题,在对instance对象做判断时由于并发导致出现和抢气球案例一样的问题。为了解决这个问题,使用双重检查加锁机制来解决。

双重检查加锁机制

使用“双重检查加锁”机制实现的程序,既能实现线程安全,有能够使性能不受较大的影响。那么何谓“双重检查加锁”机制?其指的是:

并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法后,先检查实例是否存在,如果不存在才执行同步代码块,这是**第一重检查;

进入同步块后,再次检查实例是否存在,如果不存在,就在同步块中创建一个实例,这是第二重检查。

这样,就只需要同步一次,减少了多次在同步情况判断所浪费的时间。

“双重检查加锁”机制的实现需要volatile关键字的配合使用,且Java 版本需要在Java 5及以上,虽然该机制可实现线程安全的单例模式,也要根据实际情况酌情使用,不宜大量推广使用。

使用“双重检查加锁”机制改写后的懒汉模式,代码如下:

public class SlackerDemo {

private SlackerDemo() {}

private static SlackerDemo instance = null;

public static SlackerDemo getInstance() {

if (instance == null) {

synchronized (SlackerDemo.class) {

if (instance == null) {

instance = new SlackerDemo();

}

}

}

return instance;

}

}

Lock 锁机制

java.util.concurrent.locks包提供了Lock接口,Lock锁机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,而且功能比synchronized代码块和synchronized方法更加强大。

官方的提供了参考价值很大的demo,能够很好的提现Lock机制的功能:

使用Lock 机制改写的抢气球案例代码如下所示:

import java.util.concurrent.locks.*;

public class LockDemo {

public static void main(String []args) {

Balloon balloon = new Balloon();

new Thread(balloon, "小红").start();

new Thread(balloon, "小强").start();

new Thread(balloon, "小明").start();

}

}

// 气球

class Balloon implements Runnable {

private int num = 500;

private final Lock lock = new ReentrantLock(); // 创建锁对象

@Override

public void run() {

for (int i = 0; i < 500; i++) {

grabBalloon();

}

}

// 抢气球

private void grabBalloon() {

lock.lock(); // 获取锁对象

if (num > 0) {

try {

System.out.println(Thread.currentThread().getName() + "抢到了"

+ num + "号气球");

num--;

} catch (Exception e) {

} finally {

lock.unlock(); // 释放锁

}

}

}

}

案例运行正常。

完结。老夫虽不正经,但老夫一身的才华!关注我,获取更多编程科技知识。

java 同步解决不安全类_「JAVA」Java 线程不安全分析,同步锁和Lock机制,哪个解决方案更好...相关推荐

  1. java正则匹配英文句号_「正则表达式」王国奇遇记

    第一回 初来乍到 NPC: "欢迎来到正则表达式的国度,勇士!这里的每一个人都使用正则表达式,我是这里的 NPC,每一个来到这里的人都将由我代为介绍正则世界的规则,至于能领悟到何种境界,就看 ...

  2. java 线程间通信方式_「转」JAVA多线程之线程间的通信方式

    1. 同步 这里讲的同步是指多个线程通过synchronized关键字这种方式来实现线程间的通信. public class MyObject { synchronized public void m ...

  3. python 中主线程结束 子线程还在运行么_「干货」python线程笔记

    引言&动机 考虑一下这个场景,我们有10000条数据需要处理,处理每条数据需要花费1秒,但读取数据只需要0.1秒,每条数据互不干扰.该如何执行才能花费时间最短呢? 在多线程(MT)编程出现之前 ...

  4. java实训遇到问题解决_「instantiationexception」关于java出现 java.lang.InstantiationException异常的分析与解决方案 - seo实验室...

    instantiationexception java.lang.instantiationexception 是指不能实例化某个对象,一般在我们使用java反射机制去创建某个对象的时候实例化到了一个 ...

  5. java类初始化顺序_「漫画」Java中的父子类的执行顺序到底是怎么一回事?

    ----下课后---- 结论 类的加载顺序. (1) 父类静态代码块(包括静态初始化块,静态属性,但不包括静态方法) (2) 子类静态代码块(包括静态初始化块,静态属性,但不包括静态方法 ) (3) ...

  6. java简单编程_「4」Java简单编程

    1.第一个Java程序 ●Java程序开发步骤: ●通过文本编辑器来编写java代码至扩展名为java的文件中. ●调用编译工具javac.exe来对java源文件进行编译,生成扩展名为class的字 ...

  7. eclipse折叠if语句块_「03」java中的方法以及控制语句

    语句块(有时叫做复合语句),是用花括号扩起的任意数量的简单Java语句.块确定了局部变量的作用域.块中的程序代码,作为一个整体,是要被一起执行的.块可以被嵌套在另一个块中,但是不能在两个嵌套的块内声明 ...

  8. java static 修饰_「static」JAVA static是什么?修饰符总结整理 - seo实验室

    static 对java中修饰符的总结整理,尤其是static的理解,static总是忘记 其他修饰符修饰的变量有效范围如下: 类内 package内 子类 其他package private yes ...

  9. java继承调用先后_「继承顺序」JAVA继承顺序 - seo实验室

    继承顺序 当使用继承这个特性时,程序是如何执行的: 继承的初始化顺序 1.初始化父类再初始子类 2.先执行初始化对象中属性,再执行构造方法中的初始化 当使用继承这个特性时,程序是如何执行的呢,也就是说 ...

最新文章

  1. 智在生活 自在慵懒 科沃斯机器人X京东大牌秒杀日主题展亮相无锡
  2. 不使用临时表,仅使用select实现查询出多行常数
  3. 泛型Dictionary的用法详解
  4. linux adb 端口,linux 无法连接adb 设备
  5. Java Pattern Matcher 正则表达式需要转义的字符
  6. 关于 react的生命周期
  7. RE-Base64编码分析
  8. 【长难句分析精讲】状语从句
  9. 学3D建模,都需要哪些基础?
  10. 南方周末:股神炒股一周年祭 24万本金仅剩7千
  11. OpenOCD failed tor read memory at $addr 错误
  12. Vue源码翻译之渲染逻辑链
  13. 制动电阻器的工作原理及应用特点
  14. python 7周自学计划
  15. 成都java培训一般需要多久
  16. 华为微认证——鲲鹏处理器
  17. Linux 从入门到了解
  18. 什么软件查C语言答案,C语言小测验和参考答案
  19. Learning to See before Learning to Act: Visual Pre-training for Manipulation
  20. git的搭建与简单实用

热门文章

  1. 使用Nginx代理kkFileView
  2. 微信扫描二维码,实现自动登录
  3. BI技巧丨百分位计算
  4. html如何批量替换图片,如何批量替换wps文字中的图片?
  5. 第2章第12节:标题设计技巧:制作漂亮的虚线描边文字 [PowerPoint精美幻灯片实战教程]
  6. 纸鸢|为什么我们不建议设备厂商自己研发物联网云平台?
  7. vue+mock+axios模拟post请求
  8. 4.C++设计模式-结构型模型
  9. 11.5 Vue day06 父子组件通信、自定义事件
  10. Linux后台运行Java项目世上最详解