1.多线程之cas分析
1.1什么是cas
CAS英文翻译为:Compare and Swap ,就是比较并且交换,cas有三个值,第一个是变量地址,第二个是变量旧值,第三个变量要变成的值
1.2cas在什么地方用到了
CAS无锁实现方式,在Java中的java.util.concurrent.atomic包中用到了,以AtomicInteger为例子:
//给int值加一并且得到加一后的值
public final int getAndSet(int newValue) {
return unsafe.getAndSetInt(this, valueOffset, newValue);
}
这里unsafe.getAndSetInt()的方法就是使用的cas机制,就是一个do{}while()循环,具体实现原理是:拿内存地址找到这个变量,然后旧值和内存中的值比较,如果相等了就会替换成新值。在concurrent包中大量使用了cas机制,包括重入锁,也是使用cas机制来保证锁的唯一性,至于多线程同时访问一个变量的控制,这个是CPU底层指令控制的。
1.3cas的优势和劣势
CAS的优势就是可避免死锁,根本不存在锁,只是比较内存值,所以不会出现死锁。
CAS的弊端就是可能会出现aba的情况,如果加上版本号那么这种问题就完美解决了,这就是乐观锁,Mysql的乐观锁机制就是一个版本号,还有redis的锁,都是基于版本号的乐观锁。
2.线程状态
线程的五大状态分别是:
1. 新建状态 : 刚new出线程并未执行start()方法。
2. 就绪状态 :执行了start()方法,但是还没有分配cpu给它
3. 执行状态 : 执行run()里面的方法
4. 堵塞状态 : 当线程执行sleep或者获取别人正在占有的锁的时候
5. 死亡状态 : 线程执行完毕,进入死亡状态,当时当前对象未必死亡
在实际的项目中,往往会因为某种错误操作导致线程堵塞,会造成服务器雪崩,如果有特定的访问的话,找到代码BUG是不太好找的,就可以使用线程Dump来诊断:
public void monitor(){
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfo = threadMXBean.dumpAllThreads(false, false);
for(int i=0;i<threadInfo.length;i++){
ThreadInfo threadInfo2 = threadInfo[i];
String threadName = threadInfo2.getThreadName();
State threanState = threadInfo2.getThreadState();
String blockedMsg = "";
for(int z=0;z<threadInfo2.getStackTrace().length;z++){
if(threadInfo2.getStackTrace()[z].getClassName().contains("yizhengqiyuan") && !threadInfo2.getStackTrace()[z].getClassName().contains("LogConsole")){
blockedMsg+=",阻塞类为:"+threadInfo2.getStackTrace()[z].getClassName()+"第"+threadInfo2.getStackTrace()[z].getLineNumber()+"行进行了阻塞";
}
}
// thread state NEW RUNNABLE BLOCKED WAITING TIMED_WAITING TERMINATED
if(threadName.startsWith("/") && (threanState == State.BLOCKED || threanState == State.TIMED_WAITING) ){
logger.info("线程的名字为:"+threadInfo2.getThreadName()+",线程的id为"+threadInfo2.getThreadId()+",线程的状态为:"+threadInfo2.getThreadState()+",改线程等待的时间为:"+threadInfo2.getWaitedTime()+"ms,改线程阻塞的时间为:"+threadInfo2.getBlockedTime()+"ms"+",线程阻塞信息为:"+blockedMsg);
}
}
}
3.开启线程和结束线程
要开启线程,首先要实现多线程,实现多线程有三种方法:
1. 实现Runnable接口,重写run方法
2. 实现Callable接口,重写call方法
3. 继承Thread类,重写run方法
需要注意的是,第三种方式也是实现了Runnable接口,区别就是如果有一个继承类了,你就得实现Runnable接口,因为,一个类只可以有一个父类,可以实现多个接口。callable可以做到有返回值,经典的future模式。
所谓启动多线程,在实际项目中线程一般是用线程池来控制,单纯的线程启动的代码:
Thread thread = new Thread(new Test1());
thread.start();
实现Callable接口的简单demo如下:
package com.study.www.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class ThreadNewTest extends Thread {
public static void main(String[] args) throws Exception {
ExecutorService executors = Executors.newFixedThreadPool(5);
for(int i=0;i<10;i++){
Future<String> str = executors.submit(new Test());
System.out.println(str.get());
}
executors.shutdown();//关闭线程池
while(true){
if(executors.isTerminated()){//判断线程是否执行逻辑完毕
System.out.println("线程执行逻辑完毕");
return ;
}
}
}
}
class Test implements Callable<String>{
public String call() throws Exception {
return "String1";
}
}
终止线程有三种办法:
1. 如果线程是while循环,当道某一个条件需要退出的时候可以使用标志位,当需要结束的时候只要设置标志位就可以
2. 使用stop()方法,此方法不建议用
3. 中断,thread.interrupt(); ,while循环的时候判断thread.isInterrupted()来结束线程
4.多线程之线程之间通信
4.1wait()和notify()/notyfyAll()
wait()方法和notify()方法是Object对象中的方法,也就是说所有的java类都会有这两个方法。
wait():等待对象的同步锁,必须先获取到锁,然后才可wait(),否则编译可通过,运行的时候是会抛出异常的,改方法会释放锁。
notify()/notifyAll():唤醒该对象的一个(notify())执行线程(notifyAll()所有执行线程),该方法调用必须等到获取该对象的锁之后才可执行。
package com.study.www.thread;
public class ThreadStatueTest {
public static void main(String[] args) {
ThreadAddInt b = new ThreadAddInt();
b.start();
synchronized (b)
{
try {
b.wait();
System.out.println("唤醒之后执行的逻辑");
} catch (InterruptedException e) {
}
}
System.out.println("执行完毕totalInt = " + b.totalInt);
}
}
class ThreadAddInt extends Thread {
public Integer totalInt =0;
public void run() {
synchronized (this) {
for (int i = 0; i < 100; i++) {
totalInt = totalInt+1;
System.out.println("线程正在执行,还没有被唤醒,totalInt的值为"+totalInt);
}
System.out.println("线程即将被唤醒");
notify();
}
}
}
只是做了notify的demo,notify和notifyAll的区别是notify只唤醒一个线程,notifyAll唤醒改对象的所有线程,至于唤醒顺序,需要JVM去决定。
4.2挂起和唤醒
线程挂起是suspend()方法,唤醒是resume()方法,suspend和resume必须成对出现,若只执行了suspend()方法,那么该线程就会一直持有该对象的锁。和wait/notify不同的是wait执行完synchronized括号里的内容后悔释放锁。
线程挂起和唤醒在jdk1.6中已经不推荐使用了,当某一线程A挂起了,并不会释放对象锁,线程B想恢复线程A,并且想在resume()方法之前获取锁资源的时候就会发生死锁。
4.3信号量和发令枪
发令枪(countdownlatch),通常用在线程之间交互,比如有六个线程,当第六个线程是在前五个线程执行完之后才执行的,就可以在前五个线程里对countDownlatch进行减一,在第六个线程那进行await();
countDownlatch的demo:
public class CountDownLatchTest {
static CountDownLatch countDownLatch = new CountDownLatch(5);
public static void main(String[] args) {
RunableTest runableTest = new RunableTest();
for(int i =0;i<5;i++){
Thread thread = new Thread(runableTest);
thread.start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发令枪被唤醒后执行的逻辑");
}
}
class RunableTest implements Runnable {
static int addInt = 0;
public synchronized void run() {
System.out.println("线程"+addInt+"正在执行");
addInt = addInt+1;
CountDownLatchTest.countDownLatch.countDown();
}
}
信号量(Semaphore),一个信号计数器,就像一个饭店,固定有50张桌子,也就只能坐50个人,其他人只能在某个人吃完饭了,才能进来吃饭。可以控制线程对资源的访问
package com.study.www.thread;
import java.util.concurrent.Semaphore;
public class SemaphoreTest {
static Semaphore semaphore = new Semaphore(2);
public static void main(String[] args) {
RunableTest1 runableTest1 = new RunableTest1();
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(runableTest1);
thread.setName(i+"");
thread.start();
}
}
}
class RunableTest1 implements Runnable {
public void run() {
try {
SemaphoreTest.semaphore.acquire();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("线程" + Thread.currentThread().getName() + "进来了");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("线程" + Thread.currentThread().getName() + "出去了");
SemaphoreTest.semaphore.release();
}
}
可以从执行结果上看到,每次最多有两个线程在执行
4.4CyclicBarrier回环栅栏
CyclicBarrier字面意思回环栅栏,通过他可以让一组线程等待直某个状态的时全部同时执行,回环是可重复使用。可用于多线程计算数据,最后合并计算结果。比如计算成绩,数学,语文,英语这张卷分别为三个线程,统计所有题得分加起来的分数,计算总成绩的时候就需要前面三门课程成绩计算完毕,然后根据前面三门课程各自的成绩加起来即可。
用法如下:
package com.study.www.thread;
import java.util.Random;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
/**
* 回环栅栏
* @author liuguoqiang
*
*/
public class CyclicBarrierTest {
static CyclicBarrier barrier = new CyclicBarrier(5);
public static void main(String[] args) {
CyclicBarrierRunable cyclicBarrierRunable = new CyclicBarrierRunable();
for(int i=0;i<10;i++){
Thread thread = new Thread(cyclicBarrierRunable);
thread.start();
}
}
}
class CyclicBarrierRunable implements Runnable {
public void run() {
try {
Thread.sleep(new Random().nextInt(1000));
System.out.println("线程执行到这里,我在处理线程里的业务1完毕,等待其他线程处理");
CyclicBarrierTest.barrier.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (BrokenBarrierException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("其他线程也执行完了业务1,继续处理其他业务");
}
}
countDownlatch 和 CyclicBarrier的区别:
1. countDownLatch只能使用一次,CycliBarrier可以多次重复使用,直接重置调用reset()即可;
2. countDownLatch是减数器,countDown是减一,barrier.await();是加一;
3. countDownLatch是减到0的时候执行,CycliBarrier是加到某一个数的时候执行;
4. 功能基本相同,用法上除了可复用这个特点其他基本可以实现相同的功能;
上面分析了线程基本知识,下篇文章分析锁和线程安全的集合/map/队列.