首页 > 其他分享 >线程安全问题和synchronized关键字

线程安全问题和synchronized关键字

时间:2022-11-05 17:23:38浏览次数:53  
标签:count synchronized t2 t1 关键字 线程 public

当多线程对共享变量有读写操作时,可能会产生指令交错,这样就会有线程安全问题,所以产生线程安全问题有两个前提

存在在多个线程间共享的变量

对共享变量有读写操作,如果都是读的操作就没有线程安全问题。

一、线程安全问题演示

public class Test4 {

    public static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    count++;
                }
            }
        }, "t1");

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    count--;
                }
            }
        }, "t2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(count);

    }
}

在上边的代码中,线程t1,t2对共享变量count进行操作,t1在自增,t2在自减。如果没有线程安全问题,加减各1000次后结果会是0,但代码实际执行起来因为线程安全问题结果不一定是0

二、问题分析

注意在java中自增和自减不是原子操作,也就是++,--虽然是一句代码但不是由一条执行完成的。

其中++操作会分成这几条指令:

(1) 读取count的值

(2) 准备常数1

(3) 做加1操作

(4) 把结果写入变量count

同样的--操作也会有这几条指令

(1) 读取count的值

(2) 准备常数1

(3) 做减1操作

(4) 把结果写入变量count

当两个线程分别执行自增和自减时,因为cpu交错执行指令,上边的8条指令可能会发生交叉,我们先来分析下分别执行1次加和减时的情况

如果没有线程安全问题,加1再减一后得到的应该是0,按上边的分析,在cpu交错执行8条指令后得到了1,循环执行1000次加减操作后结果就不可控了,有可能t1已经执行了多次加1操作了但是没有写,这时切到t2了,t2执行减1后还没写,又切到t2,这样来回切换结果就不确定了。

这就是这个案例中的线程安全问题的分析,造成这种问题的本质是因为自增和自减操作不是原子操作需要多个指令来完成,而多个线程执行时这些指令之间会产生交错执行。

三、用synchronized解决线程安全问题

上边案例的问题是因为自增和自减操作不是原子操作,如果可以把这俩操作变成原子操作就能解决线程安全问题。

java中提供了synchronized关键字可以来解决线程安全问题 ,基本语法如下

synchronized(锁对象){
    //非原子性的操作(一个或者多个都行)
    i++
}

可以简单理解成每个线程只有持有了锁对象时才能进到代码块中去执行,执行完代码块后会释放锁。

假设线程t1获取了锁正在执行代码块中的i++,即使它的4条指令还没执行完,这时发生了上下文切换,t2开始执行,t2执行到代码块时发现获取不到锁就不能执行代码块中的内容,t2会进入阻塞状态。

只有等t1把它的四条指令执行完并释放了锁后t2才会被叫醒去执行代码块中的内容,这样就保证了8条指令不会交错执行,因此能解决线程安全问题。

代码演示

public class Test4 {

    public static int count = 0;

    public static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    synchronized (lock){
                        count++;
                    }
                }
            }
        }, "t1");

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    synchronized (lock){
                        count--;
                    }
                }
            }
        }, "t2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(count);

    }
}

上边用synchronized包裹了非原子的操作,所以不会有线程安全问题。其中用到的锁对象可以是java中的任意对象,但要保证需要控制的两段代码块中synchronized用的是同一个锁对象,这样才能出现互斥执行的效果。

四、synchronized使用过程中的注意点

synchronized不仅可以使用在代码块上,也可以使用在方法上,以代码块的形式使用时需要执行锁对象,加在方法上则不需要执行锁对象。

4.1 使用在实例方法上

synchronized使用在实例方法上时锁住的是当前对象,即通过哪个对象调用方法就锁定它。

public class Test5 {
    public static void main(String[] args) throws InterruptedException {
        Person person = new Person();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++) {
                    person.increment();
                }
            }
        },"t1");
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++) {
                    person.decrement();
                }
            }
        },"t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(Person.count);
    }
}
class Person {
    public static int count=0;

    public synchronized void increment(){
        count++;
    }

    public synchronized void decrement(){
        count--;
    }
}

上边的代码,synchronized锁住的是person这个对象

4.2 使用在静态方法上

synchronized用在静态方法上锁住的是类的class对象

class Person {
    public static int count=0;

    public static synchronized void increment(){
        count++;
    }
    //这两个方法的加锁方式是等价的
    public static void increment2(){
        synchronized (Person.class){
            count++;
        }
    }
}

五、synchronized的原理

synchronized是如何实现锁定同步代码块中的代码,同时只允许一个线程执行的效果呢?

其实依赖与操作系统提供的Monitor对象和java的对象头。

Monitor对象有几个属性:owner,entrylist,waitset

java中每一个对象都会有对象头,对象头中有一段内容叫做markword,

当线程t1执行到同步代码块时,会先根据synchronized锁住的对象即锁对象的markword来看这个锁对象是否已经关联了一个Monitor对象,如果没有就会从操作系统获取一个Monitor对象,把它的owner设置成t1,把它的地址存在锁对象的markword里,这样线程t1就持有了锁;

当线程t2执行到同步代码块时,先根据锁对象的markword找到monitor对象,发现owner已经有人了,t2就会进入entrylist等待,t2的状态变成Blocked,如果有t3也执行同步代码块最终t3也会进入entrylist等待;

当线程t1执行完同步代码块的内容后就会清空monitor对象的owner,然后唤醒entrylist中的线程来重新竞争owner,谁竞争到owner就可以执行同步代码块中的内容,剩下的线程继续blocked

六、jdk中线程安全的类

线程安全的类指的是多个线程同时调用同一个实例的某一个方法时是线程安全的,但如果一个线程组合调用类中的多个方法是不能保证线程安全的。

6.1 Hashtable

Hashtable是一个线程安全的map实现,它其中的方法都加了synchronized关键字,锁住的是当前实例对象,所以如果多个线程调用同时某一个方法时,synchronized保证了只有一个线程能执行同步代码块中的内容,不会有线程安全问题。

但如果是方法的组合调用,则不一定。

// map是Hashtable类型的 
if(map.get("key1") == null){
      map.put("key1", System.currentTimeMillis()+"");
 }

上边这段代码被多线程调用时,get,put本身都能保证是原子操作,但组合起来就不是了。

假设t1先执行完map.get判断,结果为true,然后线程上下文

切换t2开始执行,t2也执行完map.get判断,结果为true,然后t2给map中put了key1,然后切换到t1,t1又给map中put了key1,这样就会产生key覆盖的问题。

所以说线程安全类中的多个方法组合起来不是线程安全的。

6.2 String

String是线程安全的,因为String对象的内容不能改变,多个线程对它实际上只能读,所以它是线程安全的。

6.3 StringBuffer

StringBuffer中的方法都加了Synchronized关键字,所以是线程安全的。

标签:count,synchronized,t2,t1,关键字,线程,public
From: https://www.cnblogs.com/chengxuxiaoyuan/p/16860612.html

相关文章

  • Java 多线程写zip文件遇到的错误 write beyond end of stream!
    最近在写一个大量小文件直接压缩到一个zip的需求,由于zip中的entry每一个都是独立的,不需要追加写入,也就是一个entry文件,写一个内容,因此直接使用了多线程来处理,结果就翻......
  • 我看谁还不懂多线程之间的通信+基础入门+实战教程+详细介绍+附源码
    一、多线程之间的通信(Java版本)1、多线程概念介绍多线程概念在我们的程序层面来说,多线程通常是在每个进程中执行的,相应的附和我们常说的线程与进程之间的关系。线程......
  • 同步异步,单线程多线程
    同步:同步的思想是:所有的操作都做完,才返回给用户。这样用户在线等待的时间太长,给用户一种卡死了的感觉,但是程序还在执行。这种情况下,用户不能关闭界面,如果关闭了,程序就中断......
  • Java创建线程
    线程创建:ThreadRunnabelCallableTreadclass自定义线程类继承Tread类重写run()方法,编写线程执行体创建线程对象,调用start()方法启动线程publicclassmyTre......
  • C#之跨线程访问控件属性
    在窗体设计中,会经常遇到跨线程访问窗体控件,如果直接访问会报错,那怎么办呢?直接上代码代码为一个类,实际运用的时候直接实例化调用即可  1classCrossThreadUp......
  • Python 爬虫之多线程
    网络爬虫(又被称为网页蜘蛛,网络机器人,在FOAF社区中间,更经常的称为网页追逐者),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。另外一些不常使用的名字还有蚂蚁、......
  • 首选线程池,而不是多线程; 创建线程的方法; 存储过程和for循环插入数据; String字符串一般
    首选线程池,而不是多线程首选线程池,而不是多线程/**corePoolSize:线程长期为维持线程数核心线程数,常用线程数maximumPoolSize:线程数的上限,最大线程数keepAliveTime:超过线......
  • 使用多线程可能带来什么问题?
    并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、死锁还......
  • 动态线程池使用
    1、复制到项目lib下2、File->ProjectStructure...->Modeles ->Dependencies找到目录下的jar包引用进去3、配置pom.xml<!--动态线程池配置begin--><dependency>......
  • 10-jmeter-初识负载测试的概念:逐步加压(阶梯式线程组)
    一、阶梯式线程组:jp@gc-SteppingThreadGroup(deprecated)->设计场景1、安装插件->将jmeter-plugins-manager-1.3放在ext目录下2、启动jmeter->在JMeterPluginsMa......