若是多线程下使用这个类,不过多线程如何使用和调度这个类,这个类老是表示出正确的行为,这个类就是线程安全的。git
类的线程安全表现为:github
不作正确的同步,在多个线程之间共享状态的时候,就会出现线程不安全。面试
全部的变量都是在方法内部声明的,这些变量都处于栈封闭状态。算法
好比下面的例子,a和b都是在方法内部定义的,没法被外部线程所访问,当方法结束后,栈内存被回收,因此是线程安全的。后端
void fun(){ int a = 1; int b= 2; // do something }
没有任何成员变量的类,就叫无状态的类,这种类不存在共享的资源,显然是安全的。安全
public class StatelessClass { public int service(int a,int b) { return a*b; } }
让状态不可变,两种方式:性能优化
下面例子中的,成员变量都是final而且也没有提供给外部修改变量的地方,所以是线程安全的。多线程
public class ImmutableFinal { private final int a; private final int b; public ImmutableFinal(int a, int b) { super(); this.a = a; this.b = b; } public int getA() { return a; } public int getB() { return b; } }
下面的例子中,虽然User成员变量是final的,没法修改引用。可是外部依然能够经过getUser获取到User的引用以后,修改User对象。并发
public class ImmutableFinalRef { private final int a; private final int b; private final User user;//这里就不能保证线程安全了 public ImmutableFinalRef(int a, int b) { super(); 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) { ImmutableFinalRef ref = new ImmutableFinalRef(12,23); User u = ref.getUser(); //u.setAge(35); } }
volitile在ConcurrentHashMap等并发容器中都有使用,用于保证变量的可见性。最适合一个线程写,多个线程读的情景。app
加锁能够显示地控制线程对类的访问,使用正确能够保证线程安全。
CAS操做经过不断的循环对比,试图对目标对象进行修改,也能保证线程安全。普遍用于JDK并发容器的实现中。
类中持有的成员变量,特别是对象的引用,若是这个成员对象不是线程安全的,经过get等方法发布出去,会形成这个成员对象自己持有的数据在多线程下不正确的修改,从而形成整个类线程不安全的问题。
这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get与set等访问接口与方法,这些方法为使用该变量的每一个线程都存有一份独立的副本,所以get老是返回由当前执行线程在调用set时设置的最新值。
当某个线程初次调用ThreadLocal.get方法时,就会调用initialValue来获取初始值。从概念上讲,你能够将ThreadLocal
死锁是指两个或两个以上的进程在执行过程当中,因为竞争资源或者因为彼此通讯而形成的一种阻塞的现象,若无外力做用,它们都将没法推动下去。此时称系统处于死锁状态或系统产生了死锁。
死锁的根本成因:获取锁的顺序不一致致使。
能够利用下面的示意图帮助理解:
下面的程序中,两个线程分别获取到了first和second,而后相互等待,产生了死锁。
public class DeadLockSample extends Thread { private String first; private String second; public DeadLockSample(String name, String first, String second) { super(name); this.first = first; this.second = second; } public void run() { synchronized (first) { System.out.println(this.getName() + " obtained: " + first); try { Thread.sleep(1000L); synchronized(second) { System.out.println(this.getName() + " obtained: " + second); } } catch (InterruptedException e) { // Do nothing } } } public static void main(String[] args) throws InterruptedException { String lockA = "lockA"; String lockB = "lockB"; DeadLockSample t1 = new DeadLockSample("Thread1", lockA, lockB); DeadLockSample t2 = new DeadLockSample("Thread2", lockB, lockA); t1.start(); t2.start(); t1.join(); t2.join(); } }
Debug时可使用 jps 或者系统的 ps 命令、任务管理器等工具,肯定进程 ID。其次,调用 jstack 获取线程栈,jstack your_pid. jstack 自己也会把相似的简单死锁抽取出来,直接打印出来。
若是是开发本身的管理工具,须要用更加程序化的方式扫描服务进程、定位死锁,能够考虑使用 Java 提供的标准管理 API,ThreadMXBean,其直接就提供 findDeadlockedThreads() 方法用于定位,上面的例子中用到了这个方法。
若是可能的话,尽可能避免使用多个锁,而且只有须要时才持有锁。
若是必须使用多个锁,尽可能设计好锁的获取顺序。若是对于两个线程的状况,能够参考以下的实现:
在实现转帐的类时,为了防止因为相互转帐致使的死锁,下面的实现中,经过对比帐户的hash值来肯定获取锁的顺序。当二者的hash值相等时,虽然这种状况很是少见,使用了单独的锁,来控制两个线程的访问顺序。
注意System.identityHashCode()是JDK自带的hash实现,在绝大部分状况下,保证了对象hash值的惟一性。
public class SafeOperate implements ITransfer { private static Object tieLock = new Object();//加时赛锁 @Override public void transfer(UserAccount from, UserAccount to, int amount) throws InterruptedException { int fromHash = System.identityHashCode(from); int toHash = System.identityHashCode(to); //先锁hash小的那个 if(fromHash<toHash) { synchronized (from){ synchronized (to){ System.out.println(Thread.currentThread().getName() +" get"+to.getName()); from.flyMoney(amount); to.addMoney(amount); } } }else if(toHash<fromHash) { synchronized (to){ Thread.sleep(100); synchronized (from){ from.flyMoney(amount); to.addMoney(amount); } } }else {//解决hash冲突的方法 synchronized (tieLock) { synchronized (from) { synchronized (to) { from.flyMoney(amount); to.addMoney(amount); } } } } } }
相似 Object.wait(…) 或者 CountDownLatch.await(…),都支持所谓的 timed_wait,咱们彻底能够就不假定该锁必定会得到,指定超时时间,并为没法获得锁时准备退出逻辑。
并发 Lock 实现,如 ReentrantLock 还支持非阻塞式的获取锁操做 tryLock(),这是一个插队行为(barging),并不在意等待的公平性,若是执行时对象刚好没有被独占,则直接获取锁。
标准的使用流程以下:
while(true) { if(A.getLock().tryLock()) { try { if(B.getLock().tryLock()) { try { //两把锁都拿到了,开始执行业务代码 break; }finally { B.getLock().unlock(); } } }finally { A.getLock().unlock(); } } // 很是重要,sleep随机的时间,以防两个线程谦让,产生长时间的等待,也就是活锁 SleepTools.ms(r.nextInt(10)); }
活锁偏偏与死锁相反,死锁是你们都拿不到资源都占用着对方的资源,而活锁是拿到资源却又相互释放不执行。当多线程中出现了相互谦让,都主动将资源释放给别的线程使用,这样这个资源在多个线程之间跳动而又得不到执行,这就是活锁。
在上面解决死锁的第四个方案中,为了不活锁,采用了随机休眠的机制。
线程执行中有线程优先级,优先级高的线程可以插队并优先执行,这样若是优先级高的线程一直抢占优先级低线程的资源,致使低优先级线程没法获得执行,这就是饥饿。固然还有一种饥饿的状况,一个线程一直占着一个资源不放而致使其余线程得不到执行,与死锁不一样的是饥饿在之后一段时间内仍是可以获得执行的,如那个占用资源的线程结束了并释放了资源。
对于并发控制而言,锁是一种悲观的策略,它老是假设每一次的临界区操做会产生冲突,由此,若是有多个线程同时须要访问临界区资源,则宁肯牺牲资源让线程进行等待。
无锁是一种乐观的策略,它假设对资源的访问是没有冲突的。既然没有冲突,天然不须要等待,因此全部的线程均可以在不停顿地状态下持续执行。当遇到冲突,则使用CAS来检测线程冲突,若是发现冲突,则重试直到没有冲突为止。
CAS算法的过程是,它包含三个参数CAS(V,E,N),V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才将V的值设置为N,若是V值和E值不一样,说明已经有其余线程作了更新,则当前线程什么都不作。使用CAS操做一个变量时,只有一个会胜出,并成功更新,其他均会失败。
减小锁的持有时间有助于下降锁冲突的可能性,进而提高系统的并发能力。
这种技术的典型使用场景就是ConcurrentHashMap。
对于HashMap来讲,最重要的两个方法就是get() 和put(),一种最天然的想法就是对整个HashMap加锁,必然能够获得一个线程安全的对象.可是这样作,咱们就认为加锁粒度太大.对于ConcurrentHashMap,它内部进一步细分了若干个小的hashMap,称之为段(SEGMENT).默认的状况下,一个ConcurrentHashMap被进一步细分为16个段
若是须要在ConcurrentHashMap中增长一个新的表项,并非整个HashMap加锁,而是首先根据hashcode获得该表项应该被存放到哪一个段中,而后对该段加锁,并完成put()操做.在多线程环境中,若是多个线程同时进行put()操做,只要被加入的表项不存放在同一个段中,则线程间即可以作到真正的并行。
在读多写少的场合,使用读写锁能够有效提高系统的并发能力
若是将读写锁的思想进一步的延伸,就是锁分离.读写锁根据读写锁操做功能上的不一样,进行了有效的锁分离.使用相似的思想,也能够对独占锁进行分离.
以LinkedBlockingQueue为例,take函数和put函数分别实现了冲队列取和往队列加数据,虽然两个方法都对队列进项了修改,可是LinkedBlockingQueue是基于链表的因此一个操做的是头,一个是队列尾端,从理论状况下将并不冲突
若是使用独占锁则take和put就不能完成真正的并发,因此jdk并无才用这种方式取而代之的是两把不一样的锁分离了put和take的操做
凡事都有一个度,若是对同一个锁不停地进行请求,同步和释放,其自己也会消耗系统宝贵的资源,反而不利于性能的优化。
为此,虚拟机在遇到一连串连续地对同一锁不断进行请求和释放的操做时,便会把全部的锁操做整合成对锁的一次请求,从而减小对锁的请求同步次数,这个操做叫作锁的粗化.
public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }
线程安全,而且解决了多实例的问题,可是它并不高效。由于在任什么时候候只能有一个线程调用 getInstance() 方法。
public class Singleton { private static volatile Singleton singleton = null; private Singleton() { } public static Singleton getSingleton() { if (singleton == null) { // 尽可能避免重复进入同步块 synchronized (Singleton.class) { // 同步.class,意味着对同步类方法调用 if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
在这段代码中,争论较多的是 volatile 修饰静态变量,当 Singleton 类自己有多个成员变量时,须要保证初始化过程完成后,才能被 get 到。 在现代 Java 中,内存排序模型(JMM)已经很是完善,经过 volatile 的 write 或者 read,能保证所谓的 happen-before,也就是避免常被提到的指令重排。换句话说,构造对象的 store 指令可以被保证必定在 volatile read 以前。
这种方法很是简单,由于单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,因此建立实例自己是线程安全的。
public class Singleton{ //类加载时就初始化 private static final Singleton instance = new Singleton(); private Singleton(){} public static Singleton getInstance(){ return instance; } }
public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } }
静态内部类是在被调用时才会被加载,所以它是懒汉式的。
本文由『后端精进之路』原创,首发于博客 http://teckee.github.io/ , 转载请注明出处
搜索『后端精进之路』关注公众号,马上获取最新文章和价值2000元的BATJ精品面试课程。