• 关于类的线程安全


      如果多线程下使用这个类,不过多线程如何使用和调度这个类,这个类总是表示出正确的行为,这个类就是线程安全的;不做正确的同步,在多个线程之间共享状态的时候,就会出现线程不安全;

      

      类的线程安全表现为:

    • 操作的原子性

    • 内存的可见性

     

    • 栈封闭

      所有的变量都是在方法内部声明的,这些变量都处于栈封闭状态;

     

    • 无状态

      没有任何成员变量的类

    • 让类不可变

      1. final关键字,所有的成员变量应该是私有的,同样的只要有可能,所有的成员变量应该加上final关键字,但是加上final,要注意如果成员变量又是一个对象时,这个对象所对应的类也要是不可变,才能保证整个类是不可变的;

    public class FinalRef {
    	
    	private final int a;
    	private final int b;
    	private final User user;//这里不能保证线程安全啦
    	
    	public FinalRef(int a, int b) {
    		this.a = a;
    		this.b = b;
    		this.user = new User();
    	}
    
    	public int getA() {
    		return a;
    	}
    
    	public int getB() {
    		return b;
    	}
    	
    	public User getUser() {
    		return user;
    	}
    
    	public static class User{
    		private int age;
    
    		public User(int age) {
    			super();
    			this.age = age;
    		}
    
    		public int getAge() {
    			return age;
    		}
    
    		public void setAge(int age) {
    			this.age = age;
    		}
    		
    	}
    	
    	public static void main(String[] args) {
    		FinalRef ref = new FinalRef(12,23);
    		User u = ref.getUser();
            //这里能修改user的值
    		//u.setAge(35);
    	}
    }
    

      

      2.不提供任何可供修改成员变量的地方,同时成员变量也不作为方法的返回值;

    • volatile

      volatile可用于在多线程环境下,保证类的可见性,即一个线程修改了,别的线程能够读取到,但volatile并不能保证原子性;

     

       Java内存模型规定了所有的变量都存储在主内存中;每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝(如果局部变量是一个引用类型,它引用的对象在Java堆中可被各个线程共享,但是引用本身在Java栈的局部变量表中,它是线程私有的),线程对变量的所有操作(读取,赋值等)都必须在工作内存中进行,而不能直接读写主内存的变量;不同的线程之间也无法直接访问工作内存中的变量,线程间变量的值传递均需要通过主内存完成;

       线程,主内存,工作内存三者的交换关系

         

      从变量,主内存,工作内存的定义来看,主内存主要对应于Java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域;从更低层次上说,主内存就直接对应于物理硬件的内存,而为了获取更好的运行速度,虚拟机(甚至是硬件系统本身的优化措施)可能会让工作内存优先存储与寄存器和高速缓存中,因为程序运行时主要访问读写的是工作内存;

      如下代码:运行结果是个死循环

    public class RunThread extends Thread {
        private boolean isRunning = true;
    
        public boolean isRunning() {
            return isRunning;
        }
    
        public void setRunning(boolean running) {
            isRunning = running;
        }
    
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " 进入run");
            while (isRunning) {
            }
            System.out.println(Thread.currentThread().getName() + " 线程停止");
        }
    
        public static void main(String[] args) throws InterruptedException {
            RunThread thread = new RunThread();
            thread.start();
            Thread.sleep(1000);
            thread.setRunning(false);
            System.out.println(Thread.currentThread().getName() + " 已经赋值false");
        }
    }
    

      

      变量isRunning存在于公共堆栈和线程私有堆栈中(这里的公共堆栈指的是主内存,线程的私有堆栈指的是线程的工作内存),程序运行后一直在线程的私有堆栈中取得isRunning的值为true,虽然在主线程中执行thread.setRunning(false),更新的是公共堆栈的isRunning变量,线程间对于变量的修改是无感知的,操作的是两块内存地址的数据,如下图:

        

     

      将上面代码的变量isRunning用volatile修饰,运行结果则是程序能正常退出;

     

      关于volatile与重排序,请看

     

      JVM定义的Happens-Before原则是一组偏序关系,在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作必须要存在happens-before关系,这里提到的两个操作关系可以是在一个线程内,也可以是在不同线程直接的,即对于两个操作A和B,这两个操作可以在不同的线程中执行;如果A Happens-Before B,那么可以保证,当A操作执行完后,A操作的执行结果对B操作是可见的;

     

      如果将上面的休眠注释掉,程序运行结果可能会正常结束,thread.setRunning(false)操作执行完成,isRunning为false的值能被另外的线程读取;

     

      当一个变量被volatile修饰后,它将具备两种特性,第一是保证此变量对所有线程的可见性,这里的可见性是指当一个线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的;而普通变量不能做到这一点,普通变量的值在线程间传递均需要通过主内存来完成,例如线程A修改一个普通变量的值,然后往主内存进行回写,另外一条线程B在线程A回写完成后再从主内存进行读取操作,读取至B线程的工作内存,新变量才会对线程B可见;

     

      使用volatile关键字,可以强制从主内存(公共内存)中读取变量的值,绕过了线程的工作内存,如下图:

        

     

    • CAS和加锁

      使用原子操作类,synchronized,Lock锁 

      • LockSupport

        LockSupport定义一组的公共静态方法,这些方法提供了最基本的线程阻塞唤醒功能;

    方法名称描述
    void park() 阻塞当前线程,如果调用unpark(Thread thread)方法或当前线程,才能从park()方法返回
    void parkNanos() 阻塞当前线程,最长不超过nanos纳秒,返回条件在park()的基础上增加了超时返回
    void parkUntil(long deadline) 阻塞当前线程,直到deadline时间(从1970年开始到deadline时间的毫秒数)
    void unpark(Thread thread) 唤醒处于阻塞状态的thread

          注意:阻塞状态时线程阻塞在进入synchronized关键字修饰方法或代码块(获取锁)时的状态,但是阻塞在java.concurrent包中Lock接口的线程状态却是等待状态,因为 java.concurrent包中的Lock接口对于阻塞的实现均使用LockSupport类中的相关方法

     

      • Condition接口

        任意一个Java对象,都拥有一组监视器(定义在java.lang.Object类),主要包括wait(),wiat(long timeout),notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式;Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式;

     

         Object的监视器方法与Condition接口的对比

         Condition的(部分)方法以及描述

    方法名称描述
    void await() throws InterruptedException 当前线程进入等待状态直到被通知(signal)或中断,当前线程进入运行状态且从await()方法返回的情况,包括:其他线程调用该Condition的signal()或signalAll()方法,而当前线程被选中唤醒;其他线程(调用interrupt()方法)中断当前线程;如果当前等待线程从await()方法返回,那么表面该线程已经获取了Condition对象所对应的锁
    void awaitUninterruptibly() 当前线程进入等待状态直到被通知,从方法名称上可以看出该方法对中断不敏感;
    long awaitNanos(long nanosTimeout) throws InterruptedException 当前线程进入等待状态直到被通知,中断或超时;返回值表示剩余时间,如果在nancosTimeout纳秒之前被唤醒,那么返回值就是(nanosTimeout - 实际耗时);如果返回值是0或负数,那么可以肯定已经超时
    boolean awaitUntil(Date deadline) throws InterruptedException 当前线程进入等待状态直到被通知,中断或到某个时间;如果没有到指定时间就被通知,方法返回true,否则,表示到了指定时间,方法返回false
    void signal() 唤醒一个等待在Condtion上的线程,该线程从等待方法返回前必须获得与Condition相关联得锁
    void signalAll() 唤醒所有等待在Condition上得线程,该线程从等待方法返回前必须获得与Condition相关联得锁

     

    • ThreadLocal

       使用线程本地变量

      Servlet不是线程安全类,如需共享资源,会出现线程不安全;Servlet的生命周期是接收到请求,创建一个Servlet,返回一个应答时,销毁Servlet,都是由一个线程负责的;

     

    • 死锁

      死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁

      当资源多于1个,同时小于等于竞争的线程数;获取锁的顺序不一致会导致死锁;当资源只有一个,只会产生激烈的竞争;解决方法:jstack 查看应用的锁的持有情况;保证加锁的顺序性

      动态顺序死锁,在实现时按照某种顺序加锁了,但是因为外部调用的问题,导致无法保证加锁顺序而产生的;解决: 通过内在排序,保证加锁的顺序性;也可以通过尝试拿锁;

     

    • 活锁

      活锁指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试—失败—尝试—失败的过程;处于活锁的实体是在不断的改变状态,活锁有可能自行解开;如下例子:

    /**
     *类说明:不会产生死锁的安全转账方法,尝试拿锁
     */
    public class SafeOperate implements ITransfer {
    
        @Override
        public void transfer(UserAccount from, UserAccount to, int amount)
                throws InterruptedException {
        	Random r = new Random();
        	while(true) {
        		if(from.getLock().tryLock()) {
        			try {
        				System.out.println(Thread.currentThread().getName()
        						+" get "+from.getName());
        				if(to.getLock().tryLock()) {
        					try {
        	    				System.out.println(Thread.currentThread().getName()
        	    						+" get "+to.getName());    						
        						//两把锁都拿到了
        	                    from.flyMoney(amount);
        	                    to.addMoney(amount);
        	                    break;
        					}finally {
        						to.getLock().unlock();
        					}
        				}
        			}finally {
        				from.getLock().unlock();
        			}
        		}
                //错开线程拿锁的时间
        		//Thread.sleep(r.nextInt(10));
        	}
        }
    }
    

      

      上面的线程休眠是用于错开线程拿锁的时间,休眠看起来会耗费时间,但效率会得到提高,能够减少出现重复尝试——失败的次数;

     

     

  • 相关阅读:
    使用JS实现复制粘贴功能
    前端向后端发送请求(FormData),你们不要吐槽我,有的时候我也不想写注释
    最全面的数组去重详细解析
    查找字符串数组中的最长公共前缀
    最简单的让多行表格滚动方法
    送给vue初学者的 vue.js技巧
    git 和码云的上传文件代码操作
    常用模块 二
    深拷贝与浅拷贝
    常用模块升级
  • 原文地址:https://www.cnblogs.com/coder-zyc/p/12650557.html
Copyright © 2020-2023  润新知