线程安全
- 一个类被多个线程以任意方式同时调用,且不需要外部额外同步和协同的情况下,仍然保持内部数据正确且表现正确的行为,那么这个类就是线程安全的
- 线程安全分五个等级
- 1.不可变
1.Final修饰不可变的类,如String,Integer2.枚举类,enum,可反编译查看(使用javac编译后,在使用jad查看class文件为final修饰的 2.命令: a.编译-> javac ThreadSafeEnum.java b. 反编译-> jad ThreadSafeEnum.class 3.final修饰的类和属性 a.创建对象时,使用final关键字能够使得另一个线程不会被访问到处于”部分创建”的对象,否则非final类在”部分创建”状态下被访问,是会可能发生类的泄漏 b.final只保证值不会被直接覆盖(如final 修饰list变量,无法直接赋值,可以通过add(e)方法添加元素)
- 2.线程安全
a.线程安全类的任意方法操作都不会使该对象的数据处于不一致或者数据污染的情况, b.如java并发库下的ConcurrentHashMap,LinkedBlockingQueue
- 3.有条件线程安全
- 对于单独访问类的方法,是线程安全。但对于某些复合操作,需要外部类来同步
- 如vector 的add方法,removeAllElements是线程安全的,但是putifAbsent是两个原子操作,需要添加synchronized来保证线程安全
1.add方法 public synchronized boolean add(E e){ modCount++; ensureCapacityHelper(minCapacity:element + 1); elementData[elementCount++] = e; return true; } 2.removeAllElements方法 public synchronized void removeAllElements(){ modCount++; //元素置为null,让gc进行资源回收 for(int i = 0;i<elementCount;i++){ elementData[i] = null; } elementData = 0; } 3.对于putIfAbsent,有两个方法(复合操作),需要进行synchronized进行同步操作 public synchronized void putIfAbsent(Vector<Object> vector,Object value){ If(! vector.contains(value)){ Vector.add(value); } }
- 4.线程兼容
- 正确使用同步- 用锁或synchronized代码块包含方法调用,如Collection.synchronizedList来包装List,使List的操作变为线程安全
- 5.线程对立,不管是否调用了外部同步,都不能在并发使用时保证其安全的类
变量在多线程下会被访问和修改 Public static int totalTickets = 100; Thread A : totalTickets-- Thread B : totalTickets-
- 1.不可变
线程同步、异步
同步
- 阻塞式调用,调用方必须等待响应方执行完毕后才返回(如小铺买煎饼果子,需要付完钱的同时拿到煎饼,才算完成交易)
- 大多数应用场景为非异步,如百度搜索,客户端调用服务端接口,等待服务端实时返回
- 编排流程中,必须拿到响应结果才能去做下一步操作,且在实时链路中相互之间有串联或关联数据的。如电商详情页的查询接口的内部实现
异步
- 非阻塞式调用,立即返回,调用方无需等待响应方实际结果,响应方会通过状态、通知或回调来告知对方(如淘宝购物,暂时忽视发货,物流,收货,先完成订单支付,后续通过物流信息告知交易完成)
- 异步使用场景
- 耗时任务,主线程中提交耗时任务到线程池然后通过Future来异步获取任务执行结果,这里也可以由异步任务发消息等途径来通知主线程
//固定线程数为1的线程池 ExecutorService executorService = Executor.newFixedThreadPool(nThread:1); Future<String> submit = executorService.submit(new Callable<String>(){ @overridepublic String call() throws Exception{System.out.println(“正在执行。。。。。,然后通过Future异步获取任务执行结果”); return “success”;} }); System.out.println(“正在做主线程的事情。。。。”); //通过Future获取异步任务执行的结果 String result = submit.get(); System.out.println(“拿到结果: ”+result);
- 电商下单链路的非核心链路调用,为了下单性能考虑,将订单下发的仓库等非实时流程放置后续操作,提高下单的响应速度。如江西系统通过反射消息等方式来驱动后续流程(仓储、物流、收货、派送)进行,通过异步解决链路解耦,避免立案率过长,出现故障影响用户体验
- 耗时任务,主线程中提交耗时任务到线程池然后通过Future来异步获取任务执行结果,这里也可以由异步任务发消息等途径来通知主线程
同步、异步差异
- 同步
1.优点: A.实时对结果处理,上下文始终在同一个代码块中,代码处理更加直观。 B.对错误和异常可以实时处理 2.劣势: A.耗时的接口印象整个流程的并发量
- 异步
1.优点 A.不影响主流程执行,降低响应的时间,提供应用并发量和处理能力 B.及时释放系统资源,如线程占用,内存,让系统有资源做其他事情 2.缺点 A.为保证数据最终一致性,需要对账系统去做好监控和保障 B.需要更多异步任务补偿系统间的数据一致性
阻塞、非阻塞
- 阻塞,调用结果返回之前,当前线程会被挂起。调用线程只有结果之后才返回
int port = 8080; ServerSocket server = new ServerSocket(port) //accept方法一直处于挂起状态,只有接收到相应的事件,才进行处理Socket socket = server.accept();
- 非阻塞,调用值在不能立刻得到结果之前,该调用不会阻塞当前线程,而会立即返回
Socket socket = new Socket(host:”localhost”,port:8080); BufferedWrite bwriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); String str = “hello”; //bwriter 写操作时异步,不阻塞当前线程,直接写入socketChannel,不管是否写入成功 Bwriter.write(str);
同步、异步和阻塞、非阻塞关注点
- 同步、异步
当前线程是激活的。
关注点是:接口拿到结果方式同步实现是实时返回结果,异步是通过共享变量、通知消息或回调来得到结果
- 阻塞、非阻塞
关注点:程序在等待调用结果返回时的状态
线程并发和并行
单线程程序
- 程序执行过程中始终只有一个线程在运行,后面代码必须等待前面代码执行完毕后才能执行,又叫串行执行。如下例子
//单线程下,代码从上往下串行执行 public static void main(){ int start = 1; int end = 100; int sum1 = count(start,end); System.out.println(“sum1= ”+sum1); int sum2 = count2(start,end); System.out.println(“sum2 = ”+sum2 ); } public static int count(int start,int end){ int sum = 0; for (int i = start;i<= end; i++){ Sum += i; }return sum; } public static int count2(int start,int end){ return (start + end)*(end - start+1) /2; }
多线程
- 程序有多个线程构成,方法内各代码块执行顺序不确定
//多线程下,代码不按顺序地并发执行 //通过线程池启动多个线程来并发执行 public static void main(){ int start = 1; int end = 100; ExecutorService executorService = Executors.newFixedThreadPool(nThread:2); //通过submit或execute方法将任务提交到线程池 //线程池 分配空闲的线程执行任务 executorService.execute(()->{ int sum1=count(start,end); System.out.println(“sum1 = ”+sum1);}); executorService.execute(()->{ int sum2=count2(start,end); System.out.println(“sum2 = ”+sum2);}); } public static int count(int start,int end){ int sum = 0; for (int i = start;i<= end; i++){ Sum += i; }return sum; } public static int count2(int start,int end){ return (start + end)*(end - start+1) /2; }
并发
- 逻辑上的同时处理。能在一段时间内处理多个时间,但不一定保证是同时进行(特别单核处理器,是通过CPU切片来分配执行时间)
- 只有一个处理器的多个线程,由系统调度,让不同线程在不同时间内执行各自线程任务逻辑(CPU时间片轮转)
并行
-
物理或实际的同时处理(多核CPU),能在同一个时刻(时间点)处理多个事件
-
并行具有并发的含义,但并发不一定包含并行,因为并发有可能不是同时进行
-
如下,3个处理器处理20个事件,则有3个事件同时进行,其他事件在进行并发处理
-
典型,两对列共享咖啡机。一个咖啡机(单核CPU)进行并发处理两个队列的购买请求两个咖啡机(多核CPU)并行处理两对列的购买请求
-
并行发具有并发含义,并发不一定是并行- 多核CPU能力强于单核CPU处理能力,且有更少系统调度和线程上下文切换
java 线程状态及常见方法
- java.lang.Thrad线程状态枚举类有new,runnable,blocked,waitting,
- NEW,线程被创建,但是还未启动,未调用start()
- RUNNABLE,线程被JVM正在执行,但是也可能在执行的时候等待诸如操作系统的CPU,内存等资源
- BLOCKED,线程通过调用Ojbect.wait()方法进入阻塞状态,等待获取monitor lock,以便进入或再次进入synchronized代码块进行并发业务逻辑处理
- WAITING,线程通过调用Ojbect.wait()、Thread.join()、LockSupport.park()任意一个方法进入等待状态。线程出入WAITING状态来等待其他线程执行特定动作,如一个线程已经调用wait() 方法on一个对象上面,并且其在等待另一个线程调用Object.notify()或Object.notifyAll()在这个对象上。另一种情况就是一个已经调用Thread.join()后,等待特定的线程终止;当前线程可以被其他线程通过Object.notify()、Object.notifyAll()、LockSupport.unpark(Thread)
- TIMED_WATING,类似WAITING,不同的是加了等待时间,线程Object.sleep(long time)、Ojbect.wait(long time)、Thread.join(long time)、LockSupport.park(),LockSupport.parkNanos(),LockSupport.parkUnti();当前线程可以被其他线程通过Object.notify()、Object.notifyAll()、LockSupport.unpark(Thread) - TERMIATED,终止线程的线程状态,线程已经执行完成
- 线程状态转移流程
具体线程方法
- Thread.yield(),线程让步,使用该方法,当前线程就会退出CPU时间片,让其他线程或当前线程使用CPU时间片执行
pulbic class ThreadYield{ public static Object object = new Object(); public static void main(String[] args){ newThread(threadName:”ThreadA”).start();n ewThread(threadName:”ThreadB”).start(); System.in.read(); } //创建、返回新的线程对象 private static Thread newThread(String threadName){ //返回新创建的带有代码逻辑的线程对象 return new Thread(()->{for(int i = 0;i<20;i++){ System.out.println(Thread.currentThread().getName()+”:”+i); //循环第十次,当前线程出让当前CPU时间片 if(i == 10){ Thread.yield();}} },threadName);}}
- Thread.sleep(),线程休眠,主动出让当前CPU时间,在指定的时间过后,CPU会返回继续执行该线程业务。并且,sleep方法不会释放当前对象持有的锁。
pulbic class ThreadSleep{ public static Object object = new Object(); public static void main(String[] args){ newThread(threadName:”ThreadA”).start();n ewThread(threadName:”ThreadB”).start(); } //创建、返回新的线程对象 private static Thread newThread(String threadName){ //返回新创建的带有代码逻辑的线程对象 return new Thread(()->{ synchronized(object){ for(int i = 0;i<20;i++){ System.out.println(Thread.currentThread().getName()+”:”+i); if(i==10){ try{ //如是第十轮循环,则进入调用Thread.sleep()睡眠,但睡眠时,线程不释放锁 System.out.println(Thread.currentThread().getName()+”start to sleep”); //当前线程睡眠,释放CPU时间片,等到指定时间(1000MS=1s)后返回,期间不释放锁 Thread.sleep(millis:1000L); System.out.println(Thread.currentThread().getName()+”finished sleeping”); } catch(InterruptedException e){ e.printStackTrace(); } } } } },threadName);}}
- Thread.join(),等待该线程(一般是子线程)死亡/终止,当前线程(一般是主线程main方法)会等待调用join方法的线程执行完毕后才能执行后续代码逻辑
pulbic class ThreadJoin{ public static Object object = new Object(); public static void main(String[] args){ Thread threadA = newThread(threadName:”ThreadA”)); Thread threadB = newThread(threadName:”ThreadB”); threadA.start(); threadB.start(); //hreadA调用join(),需要等待join方法执行完毕,才能执行主线程后续代码逻辑 threadA.join(); System.out.println(”等待ThreadA执行完毕/终止后,才能执行后续逻辑” ); //hreadB调用join(),需要等待join方法执行完毕,才能执行主线程后续代码逻辑 threadB.join(); System.out.println(”等待threadB执行完毕/终止后,才能执行后续逻辑” ); } //创建、返回新的线程对象 private static Thread newThread(String threadName){ //返回新创建的带有代码逻辑的线程对象 return new Thread(()-> {for(int i = 0; i<5; i++){ System.out.println(Thread.currentThread().getName()+”:”+i); //循环第2次,当前线程出让当前CPU时间片 if(i == 2){ Try{Thread.sleep(millis:1000L);} catch(InterruptedException e){e.printStackTrace(); } } } },threadName); } }
- Object.wait(),Object类的方法,调用前必须拥有对象锁,例如在synchronized代码块内,调用wait方法后,对象锁会释放,线程进入WAITING状态
pulbic class ThreadJoin{ public static Object lock= new Object(); public static void main(String[] args){ newThread(threadName:”ThreadA”)).start(); newThread(threadName:”ThreadB”).start(); System.in.read(); } //创建、返回新的线程对象 private static Thread newThread(String threadName){ //返回新创建的带有代码逻辑的线程对象 return new Thread(()->{ //对lock对象加锁,进行同步访问 synchronized(lock){ for(int i = 0; i<5; i++){ //通知、唤醒其他所有线程去竞争对象锁Lock.notifyAll(); System.out.println(Thread.currentThread().getName()+”:”+i); try{ System.out.println(Thread.currentThread().getName()+”wait startting”); Thread.sleep(millis:1000L); System.out.println(Thread.currentThread().getName()+”wait stopped”); } catch( InterruptedException e){e.printStackTrace(); } } } },threadName);}}
线程死锁和避免
- 死锁是指两个或两个以上的进程(也可换成线程)执行过程中,由于竞争资源或由于彼此通信而造成的一种阻塞线现象。若无外力作用,他们都无法进行下去。此时系统处于死锁,这些永远在互相等待的进程成为死锁
- 死锁原因,两个及以上线程,在抢占2把及以上的锁,抢占锁的顺序不一致,导致线程相互等待对方持有的锁,造成阻塞.例子
pulbic class ThreadJoin{
public static Object lock1= new Object();
public static Object lock2= new Object();
public static void main(String[] args){
//线程A获取两把锁的顺序是先获取lock1,在获取lock2
newThread(”ThreadA”,lock1,lock2)).start();
//线程B获取两把锁的顺序是先获取lock2,在获取lock1
newThread(“ThreadB”,lock2,lock1).start();
//# 由于两线程获取锁的顺序不一致,导致在获取第二把锁时,相互等待对方锁的释放,造成阻塞,产生死锁
System.in.read();
}
private static Thread newThread(
String threadName,Object lockFirst,Object lockSecond){
return new Thread(()->{
//当前对象获取第一把锁
synchronized(lockFirst){
System.out.println(Thread.currentThread().getName()+” holding LockFirst”);
try{
Thread.sleep(millis:3000L);
}catch(InterruptedException e){
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+” waiting lockSecond”);
//当前对象获取第二把锁
synchronized(lockSecond){
System.out.println(Thread.currentThread().getName()+” holding lockSecond”);
}
},threadName);}}
避免死锁的方法
- 1.不使用锁,不使用2把及以上的锁
- 2.必须使用2把及以上的锁时,确保在整个应用中获取锁的顺序是一致的
- 3.尝试编程时使用具有超时释放的锁,例如Lock中的tryLock来获取锁
- 4.当发生Java-level的锁时,重启程序来干掉进程/线程
如何追踪发现死锁
- jps, 使用该命令查看jvm所有的进程 ,或者linux系统用 ps -ef | grep java来找到出现死锁的java进程
- jstack 进程号, 打印JVM进程当前时刻的线程快照,得到当前JVM进程每一条线程正在执行的堆栈信息,等位线程卡顿问题,进而查看java-level死锁信息
数据库死锁
- 开启两个及以上事务,对同样两行进行更新时,更新顺序不一致,造成死锁
- 模拟死锁
- 1.通过mysql命令 进入后,选择一个数据库 ,use dbName;
- 2.选择某个测试数据表
- 3.开启两个会话session1、session2,在MySQL连接中,使用start transaction开启
- 4.session1更新id=1的行,未发现死锁
- 5.session2更新id=2 的行,未发现死锁
- 6.session1 更新id=1的行,此时等待id=2的锁(但是此时id=1的锁被session2持有)
- 7.session2更新id=1的行,此时数据库检测到死锁,终止触发死锁的事务,session1等待状态变化正常状态
- 8.调用commit提交session1,数据修改成功