目录

    线程安全性

    一个对象是否需要是线程安全的,取决于它是否被多个线程访问,而不取决于对象要实现的功能

    什么是线程安全的代码

    核心:对 共享的 和 可变的 状态的访问进行管理。防止对数据发生不受控的并发访问。

    何为对象的状态?

    状态是指存储在对象的状态变量(例如实例或静态域)中的数据。还可能包括 其他依赖对象 的域。

    eg:某个hashmap的状态不仅存储在hashmap对象本身,还存储在许多map.entry对象中。

    总而言之,在对象的状态中包含了任何可能影响其外部可见行为的数据。

    何为共享的?

    共享的 是指变量可同时被多个线程访问

    何为可变的?

    可变的 是指变量的值在其生命周期内可以发生变化。试想,如果一个共享变量的值在其生命周期内不会发生变化,那么在多个

    线程访问它的时候,就不会出现数据不一致的现象,自然就不存在线程安全性问题了。

    什么是线程安全性

    当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,达到预期的效果,那么就称这个类是线程安全的。

    如下启动10个线程,每个线程对inc执行1000次递增,并添加一个计时线程,预期效果应为10000,而实际输出值为6880,是一个小于10000的值,并未达到预期效果,因此ins类不是线程安全的,整个程序也不是线程安全的。原因是递增操作不是原子操作,并且没有适当的同步机制

    package hgh0808;
    public class test {
        public static void main(string[] args){
            for(int i = 0;i < 10;i++){
                thread th = new thread(new cthread());
                th.start();
            }
            timethread tt = new timethread();
            tt.start();
            try{
                thread.sleep(21000);
            }catch(exception e){
                e.printstacktrace();
            }
            system.out.println(ins.inc);
        }
    }
    ---------------------------------------------------------------------
    package hgh0808;
    import java.util.concurrent.atomic.*;
    public class timethread extends thread{
        @override
        public void run(){
            int count = 1;
            for(int i = 0;i < 20;i++){
                try{
                    thread.sleep(1000);
                }catch(exception e){
                    e.printstacktrace();
                }
                system.out.println(count++);
            }
        }
    }
    ---------------------------------------------------------------------
    package hgh0808;
    public class cthread implements runnable{
        @override
        public void run(){
            for(int j = 0;j < 1000;j++){
                ins.increase();
            }
        }
    }
    ---------------------------------------------------------------------
    package hgh0808;
    public class ins{
        public static volatile int inc = 0;
        public static void increase(){
                inc++;
        }
    }
    =====================================================================
    

    执行结果:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    6880

    通过synchronized加锁机制,对ins类实现同步,如下得到了正确的运行结果,很容易可以看出,主调代码中并没有任何额外的同步或协同,此时的ins类是线程安全的,整个程序也是线程安全的

    package hgh0808;
    public class ins{
        public static volatile int inc = 0;
        public static void increase(){
            synchronized (ins.class){
                inc++;
            }
        }
    }
    

    执行结果:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    10000

    如何编写线程安全的代码
    ————————————————————————————————
    如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误,像上文中进行同步之前的代码
    有三种方式可以修复这个问题:
    *不在线程之间共享该状态变量
    *将状态变量修改为不可变的变量
    *在访问状态变量时使用同步
    前两种方法是针对 共享 和 不变 这两个属性(见上文)解决问题,在有些情境下会违背程序设计的初衷(比如上文中ins类中的inc变量不可能不变,且在多核处理器的环境下为了提高程序性能,就需要多个线程同时处理,这样变量就必然要被多个线程共享)。
    基于此,我们针对第三种方式—— 在访问状态变量时使用同步 展开讨论
    在讨论第三种方式之前,我们先介绍几个简单的概念
    原子性 :一个操作序列的所有操作要么不间断地全部被执行,要么一个也没有执行
    竞态条件 :当某个计算的正确性取决于多个线程的的交替执行时序时,就会发生竞态条件。通俗的说,就是某个程序结果的正确性取决于运气时,就会发生竞态条件。(竞态条件并不总是会产生错误,还需要某种不恰当的执行时序)
    常见的竞态条件类型:
    *检查–执行(例如延迟初始化)
    *读取–修改–写入(例如自增++操作)
    针对以上两种常见的竞态条件类型,我们分别给出例子

    延迟初始化(检查--执行)
    --------------------------------------------------------------------
    package hgh0808;
    import java.util.arraylist;
    public class test1 {
        public arraylist<ball> list;
        public arraylist<ball> getinstance(){
            if(list == null){
                list = new arraylist<ball>();
            }
            return list;
        }
    }
    class ball{
    }
    

    大概逻辑是先判断list是否为空,若为空,创建一个新的arraylist对象,若不为空,则直接使用已存在的arraylist对象,这样可以保证在整个项目中list始终指向同一个对象。这在单线程环境中是完全没有问题的,但是如果在多线程环境中,list还未实例化时,a线程和b线程同时执行if语句,a和b线程都会认为list为null,a和b线程都会执行实例化语句,造成混乱。

    自增++操作(读取--修改--写入)
    ------------------------------------------------------------------------
    参考上文中为改进之前的代码(对ins类中inc的自增)
    

    以上两个例子告诉我们,必须添加适当的同步策略,保证复合操作的原子性,防止竞态条件的出现

    策略一:使用原子变量类,在java.util.concurrent.atomic包中包含了一些原子变量类

    package hgh0808;
    import java.util.concurrent.*;
    import java.util.concurrent.atomic.atomicinteger;
    import java.util.concurrent.atomic.atomiclong;
    
    public class ins{
        public static atomicinteger inc = new atomicinteger(0);
        public static void increase(){
            inc.incrementandget();
        }
    }
    

    执行结果:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    10000

    值得注意的是,只有一个状态变量时,可以通过原子变量类实现线程安全。但是如果有多个状态变量呢?

    设想一个情景

    多个线程不断产生1到10000的随机数并且发送到一个计算线程,计算线程每获取一个数字n,就计算sinx在[0,n]上的积分并打印到控制台上,为了提高程序性能,设计一个缓存机制,保存上次的数字n和积分结果(两个状态变量)。如果本次的数字和上次的数字相等,直接打印积分结果,避免重复计算。

    看代码:

    package hgh0808;
    import java.util.concurrent.atomic.atomicreference;
    
    public class calculate extends thread{
        private final atomicreference<double> lastnumber  = new atomicreference<double>();  //缓存机制,原子变量类
        private final atomicreference<double> lastres = new atomicreference<double>();      //缓存机制,原子变量类
        private static final double n = 100000;    //将区间[0,e]分成100000份,方便定积分运算
        public void service() throws exception{
            getdata();
            thread.sleep(10000);   //等待myqueue队列中有一定数量的元素后,再开始从其中取元素
            while(true){
                double e;
                    if(!myqueue.myisempty()){
                         e = myqueue.myremove();
                    }else{
                        return;
                    }
                if(e.equals(lastnumber.get())){
                    system.out.println(lastnumber.get()+" "+lastres.get());
                }else{
                    double temp = integral(e);
                    lastnumber.set(e);
                    lastres.set(temp);
                    system.out.println(e+" "+temp);
                }
                thread.sleep(2000);
            }
        }
        public void getdata(){   //创建并启动四个获取随机数的线程,这四个线程交替向myqueue队列中添加元素
            thread1 th1 = new thread1();
            thread2 th2 = new thread2();
            thread3 th3 = new thread3();
            thread4 th4 = new thread4();
            th1.start();
            th2.start();
            th3.start();
            th4.start();
        }
        public double integral(double e){    //计算定积分
            double step = (e-0)/n;
            double left = 0,right = step;
            double sum = 0;
            while(right <= e){
                double mid = left+(right-left)/2;
                sum+=math.sin(mid);
                left+=step;
                right+=step;
            }
            sum*=step;
            return sum;
        }
    }
    ---------------------------------------------------------------------
    package hgh0808;
    import java.util.linkedlist;
    public class myqueue {      //由于linkedlist是线程不安全的,因此需要将其改写为线程安全类
        private static linkedlist<double> queue = new linkedlist<>();
        synchronized public static void myadd(double e){
            queue.addlast(e);
        }
        synchronized public static void myclear(){
            queue.clear();
        }
        synchronized public static int mysize(){
            return queue.size();
        }
        synchronized public static boolean myisempty(){
            return queue.isempty();
        }
        synchronized public static double myremove(){
            return queue.removefirst();
        }
    }
    -----------------------------------------------------------------------
    package hgh0808;
    import java.util.random;
    public class thread1 extends thread{
        private double data;
        @override
        public void run(){
            while(true){
                random random = new random();
                data = (double) (random.nextint(10000)+1);
                if(myqueue.mysize() > 10000){     //由于从队列中取元素的速度低于四个线程向队列中加元素的速度,因此队列的长度是趋于扩张的,当达到一定程度时,清空队列
                    myqueue.myclear();
                }
                myqueue.myadd(data);
                try {
                    thread.sleep(1000);
                }catch(exception e){
                    e.printstacktrace();
                }
            }
        }
    }
    ------------------------------------------------------------------------
    package hgh0808;
    import java.util.random;
    public class thread2 extends thread{
        private double data;
        @override
        public void run(){
            while(true){
                random random = new random();
                data = (double) (random.nextint(10000)+1);
                if(myqueue.mysize() > 10000){
                    myqueue.myclear();
                }
                myqueue.myadd(data);
                try {
                    thread.sleep(1000);
                }catch(exception e){
                    e.printstacktrace();
                }
            }
        }
    }
    -----------------------------------------------------------------------
    package hgh0808;
    import java.util.random;
    public class thread3 extends thread{
        private double data;
        @override
        public void run(){
            while(true){
                random random = new random();
                data = (double) (random.nextint(10000)+1);
                if(myqueue.mysize() > 10000){
                    myqueue.myclear();
                }
                myqueue.myadd(data);
                try {
                    thread.sleep(1000);
                }catch(exception e){
                    e.printstacktrace();
                }
            }
        }
    }
    ------------------------------------------------------------------------
    package hgh0808;
    import java.util.random;
    public class thread4 extends thread{
        private double data;
        @override
        public void run(){
            while(true){
                random random = new random();
                data = (double) (random.nextint(10000)+1);
                if(myqueue.mysize() > 10000){
                    myqueue.myclear();
                }
                myqueue.myadd(data);
                try {
                    thread.sleep(1000);
                }catch(exception e){
                    e.printstacktrace();
                }
            }
        }
    }
    

    只看calculate线程,不看其他线程和myqueue中的锁机制,本问题的焦点在于calculate线程中对多个状态变量的同步策略

    存在问题:

    尽管对lastnumber和lastres的set方法的每次调用都是原子的,但仍然无法同时更新lastnumber和lastres;如果只修改了其中一个变量,那么在这两次修改操作之间,其它线程将发现不变性条件被破坏了。换句话说,就是没有足够的原子性

    **当在不变性条件中涉及多个变量时,各个变量间并不是彼此独立的,而是某个变量的值会对其它变量的值产生约束。因此当更新某一个变量时,需要在同一个原子操作中对其他变量同时进行更新。

    改进 ================>加锁机制 内置锁 synchronized

    之所以每个对象都有一个内置锁,只是为了免去显式地创建锁对象

    synchronized修饰方法就是横跨整个方法体的同步代码块

    非静态方法的锁—–方法调用所在的对象

    静态方法的锁—–方法所在类的class对象

    public class calculate extends thread{
        private final atomicreference<double> lastnumber  = new atomicreference<double>();  //缓存机制,原子变量类
        private final atomicreference<double> lastres = new atomicreference<double>();      //缓存机制,原子变量类
        private static final double n = 100000;    //将区间[0,e]分成100000份,方便定积分运算
        public void service() throws exception{
            getdata();
            thread.sleep(10000);   //等待myqueue队列中有一定数量的元素后,再开始从其中取元素
            while(true){
                double e;
                synchronized (this){    //检查--执行 使用synchronized同步,防止出现竞态条件
                    if(!myqueue.myisempty()){
                         e = myqueue.myremove();
                    }else{
                        return;
                    }
                }
                if(e.equals(lastnumber.get())){
                    system.out.println(lastnumber.get()+" "+lastres.get());
                }else{
                    double temp = integral(e);
                    synchronized (this) {     //两个状态变量在同一个原子操作中更新
                        lastnumber.set(e);
                        lastres.set(temp);
                    }
                    system.out.println(e+" "+temp);
                }
                thread.sleep(2000);
            }
        }
        public void getdata(){   //创建并启动四个获取随机数的线程,这四个线程交替向myqueue队列中添加元素
            thread1 th1 = new thread1();
            thread2 th2 = new thread2();
            thread3 th3 = new thread3();
            thread4 th4 = new thread4();
            th1.start();
            th2.start();
            th3.start();
            th4.start();
        }
        public double integral(double e){    //计算定积分
            double step = (e-0)/n;
            double left = 0,right = step;
            double sum = 0;
            while(right <= e){
                double mid = left+(right-left)/2;
                sum+=math.sin(mid);
                left+=step;
                right+=step;
            }
            sum*=step;
            return sum;
        }
    }
    

    对于包含多个变量的不变性条件中,其中涉及的所有变量都需要由同一个锁来保护

    synchronized (this) {     //两个状态变量在同一个原子操作中更新
                        lastnumber.set(e);
                        lastres.set(temp);
                    }
    

    锁的重入

    如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功,“重入”意味着获取锁的操作的粒度是‘线程’,而不是‘调用’。

    重入的一种实现方式 :

    为每个锁关联一个获取计数值和一个所有者线程。当计数值为0时,这个锁就被认为是没有被任何线程持有。当线程请求一个未被持有的锁时,jvm将记下锁的持有者,并且将获取计数值置为1。如果同一个线程再次获取这个锁,计数值将递增,当线程退出同步代码块时,计数器会相应地递减。当计数值为0时,这个锁将被释放。

    如果内置锁不可重入,那么以下这段代码将发生死锁(每个dosomething方法在执行前都会获取father上的内置锁)
    ----------------------------------------------------------------------
    public class father{
      public synchronized void dosomething(){
      }
    }
    
    public class son extends father{
       @override
       public synchronized void dosomething(){
           system.out.println("重写");
           super.dosomething();
       }
    }
    

    线程安全性与性能和活跃性之间的平衡

    活跃性:是否会发生死锁饥饿等现象
    性能:线程的并发度
    不良并发的应用程序:可同时调用的线程数量,不仅受到可用处理资源的限制,还受到应用程序本身结构的限制。幸运的是,通过缩小同步代码块的作用范围,可以平衡这个问题。
    缩小作用范围的原则====>当执行时间较长的计算或者可能无法快速完成的操作时,一定不能持有锁!!!

    总结

    本篇文章就到这里了,希望能给你带来帮助,也希望您能够多多关注www.887551.com的更多内容!