多线程核心技术(1)-线程的基本方法
进程和线程
了解多线程首先要了解进程和线程的概念,在操作系统里,进程是资源分配最小单位,一般情况下一个应用就会在计算机系统内开启一个进程,线程可以理解为进程中多个独立运行的子任务,是操作系统能够进行调度运算的最小单位,但是线程不拥有资源,只能共享进程中的数据,所以多个线程对进程中某个数据同时进行修改时,就会产生线程安全问题。由于一个进程中允许存在多个线程,所以在多线程中,如何处理线程并发和线程之间通信的问题,是学习多线程编程的重点。
复制代码
多线程的使用
在java中,创建一个线程一般有两种方式,继承Thread类或者实现Runable接口,重写run方法即可,然后调用start()方法即可以开启一个线程并执行。如果想要获取当前线程执行返回值,在jdk1.5以后,可以通过实现Callable接口,然后借助FutureTask或者线程池得到返回值。由于线程的执行具有随机性,所以线程的开启顺序并不意味线程的执行顺序。
1、继承Thread创建一个线程
/**
* 继承Thread 重写Run方法
* 类是单继承的,生产环境中如果此类无需实现其他接口 可使用这种方法创建线程
* User: lijinpeng
* Created by Shanghai on 2019/4/13.
*/
@Slf4j
public class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
log.info("Hi,I am a thread extends Thread,My name is:{}", this.getName());
}
}
复制代码
2、实现Runable接口创建一个线程
/**
* 实现Runnable接口
* 类允许有多个接口实现 生产中一般使用这种方式创建线程
* 线程的开启还需要借助于Thread实现
* User: lijinpeng
* Created by Shanghai on 2019/4/13.
*/
@Slf4j
@Getter
public class ThreadRunable implements Runnable {
private String name;
public ThreadRunable(String name) {
this.name = name;
}
public void run() {
log.info("Hi,I am a thread implements Runnable,My name is:{}", this.getName());
}
}
复制代码
3、实现Callable 接口 获取线程执行结果
/**
* 实现Callable接口创建获取具有返回值的线程
* 线程使用需要借助FutureTask和Thread,或者使用线程池
* User: lijinpeng
* Created by Shanghai on 2019/4/13.
*/
@Slf4j
public class CallableThread implements Callable<Integer> {
private AtomicInteger seed;
@Getter
private String name;
public CallableThread(String name, AtomicInteger seed) {
this.name = name;
this.seed = seed;
}
public Integer call() throws Exception {
//使用并发安全的原子类生成一个整数
Integer value = seed.getAndIncrement();
log.info("I am thread implements Callable,my name is:{} my value is:{}", this.name, value);
return value;
}
}
复制代码
4、验证三种线程的启动
/**
* User: lijinpeng
* Created by Shanghai on 2019/4/13.
*/
@Slf4j
public class ThreadTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
threadTest();
runableTest();
callableTest();
}
public static void threadTest() {
MyThread threadA = new MyThread("threadA");
threadA.start();
}
public static void runableTest() {
ThreadRunable runable = new ThreadRunable("threadB");
//需要借助Thread来开启一个新的线程
Thread threadB = new Thread(runable);
threadB.start();
}
public static void callableTest() throws ExecutionException, InterruptedException {
AtomicInteger atomic = new AtomicInteger();
CallableThread threadC1 = new CallableThread("threadC1", atomic);
CallableThread threadC2 = new CallableThread("threadC2", atomic);
CallableThread threadC3 = new CallableThread("threadC3", atomic);
FutureTask<Integer> task1 = new FutureTask<Integer>(threadC1);
FutureTask<Integer> task2 = new FutureTask<Integer>(threadC2);
FutureTask<Integer> task3 = new FutureTask<Integer>(threadC3);
Thread thread1 = new Thread(task1);
Thread thread2 = new Thread(task2);
Thread thread3 = new Thread(task3);
thread1.start();
thread2.start();
thread3.start();
while (task1.isDone()&&task2.isDone()&&task3.isDone())
{
}
log.info(threadC1.getName()+"执行结果:"+String.valueOf(task1.get()));
log.info(threadC2.getName()+"执行结果:"+String.valueOf(task2.get()));
log.info(threadC2.getName()+"执行结果:"+String.valueOf(task3.get()));
}
}
复制代码
以下是程序执行结果:
结论:
- 这三种方式都可以开启一个线程,实现具体开启线程的任务还是交给Thread类实现,因此对于Runable和Callable来说,最后都是要借助于Thread开启线程,类是单继承的,接口是多实现的,由于生产环境业务复杂性,一个类可能会有其他功能,因此一般使用接口实现的方式。
- 从上面的线程声明顺序和执行顺序结果来看,线程的执行是无序的,CPU执行任务是采用轮询机制来提高CPU使用率,在线程获取执行资源进行就绪队列后 才会再次被CPU调用,而这个过程跟程序无关。
线程的生命周期
一个线程的运行通常伴随着线程的启动、阻塞、停止等过程,线程启动可以通过Thread类的start()方法执行,由于多线程可能会共享进程数据,阻塞一般发生在等待其他线程释放进程某块资源的过程,当线程执行完毕,可以自动停止,也可以通过调用stop()强制终止线程,或者在线程执行过程中由于异常导致线程终止,了解线程的生命周期是学习多线程最重要的理论基础。
下图为线程的生命周期以及状态转换过程
新建状态
当通过Thread thead=new Thread()创建一个线程的时候,该线程就处于 new 状态,也叫新建状态。
就绪状态
当调用thread.start()时,线程就进入了就绪状态,在该状态下线程并不会运行,只是表示线程进入可供CPU调用的就绪队列,具备运行条件。
运行状态
当线程获得了JVM中线程调度器的调度时候,线程就进入运行状态,会执行重写的 run方法。
阻塞状态
此时的线程仍处于活动状态,但是由于某种原因失去了CPU对其调度权利,具体原因可分为以下几种
-
同步阻塞
此时由于线程A需要获取进程的资源1,但是资源1被线程B所持有,必须等待线程B释放资源1之后,该线程才会进入资源1的就绪线程池里,获取到资源1后,等待被CPU调度器调度再次运行。同步阻塞一般出现在线程等待某项资源的使用权利,在程序中使用锁机制会产生同步阻塞。 复制代码
-
等待阻塞
当执行Thread类的wait() 和join()方法时,会造成当前线程的同步阻塞,wait()会使当前线程暂停运行,并且释放所拥有的锁,可以通该线程要等待的某个类(Object)的notify()或者notifyall()方法唤醒当前线程。join()方法会阻塞当前线程,直到线程执行完毕,可以通过join(time)指定等待的时间,然后唤醒线程。 复制代码
-
其他阻塞
调用sleep()方法主动放弃所占用的CPU资源,这种方式不会释放该线程所拥有的锁,或者调用一个阻塞式IO方法、发出了I/O请求,进入这种阻塞状态。被阻塞的线程会在合适的时候(阻塞解除后)重新进入就绪状态,重新等待线程调度器再次调度它。 复制代码
死亡状态
当线程执行完run方法时,就会自动终止或者处于死亡状态,这是线程的正常死亡过程。或者通过显示调用stop()终止线程,但不安全。还可以通过抛异常法终止线程。
实例变量与线程安全
多线程访问进程资源
在多线程任务应用中如果多个线程执行之间使用了进程的不同资源,即运行中不共享任何进程资源,各线程运行不受影响,且不会产生数据安全问题。如果多个线程共享了进程的某块资源,会同时修改该块资源数据,产生最终结果与预期结果不一致的情况,导致线程安全问题。如图:
主内存与工作内存
Java内存模型分为主内存,和工作内存。主内存是所有的线程所共享的,工作内存是每个线程自己有一个,不是共享的。每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝。线程对变量的所有操作(读取、赋值),都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者之间的交互关系如下图:
线程对主存的操作指令:lock,unlock,read,load,use,assign,store,write操作
-
read-load阶段从主内存复制变量到当前工作内存
-
use和assign阶段执行代码改变共享变量值
-
store和write阶段用工作内存数据刷新主存对应变量的值。
-
store and write执行时机
1、java内存模型规定不允许read和load、store和write操作之一单独出现,以上两个操作必须按顺序执行,没必要 连续执行,也就是说read与load之间、store与write之间是可插入其他指令的。 2、不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。变量在当前线程中改变一次其实就是一次assign,而且不允许丢弃最近的assign,所以必定有一次store and write,又根据第一条read and load 和store and write 不能单一出现,所以有一次store and write 必定有一次 read and load,因此推断出,变量在当前线程中每一次变化都会执行 read 、 load 、use 、assign、store、write 3、volatile修饰变量,是在use和assign阶段保证获取到的变量永远是跟主内存变量保持同步 复制代码
非线程安全问题
在多线程环境下use和assign是多次出现的,但此操作并不是原子性的,也就是说在线程A执行了read和load从主内存 加载过变量C后,此时如果B线程修改了主内存中变量C的值,由于线程A已经加载过变量C,无法感知数据已经发生变化,即从线程A的角度来看,工作内存和主内存的变量A已经不再同步,当线程A使用use和assign时,就会出现非线程安全的问题。解决此问题可以通过使用volatile关键字修饰,volatile可以保证线程每次使用use和assign时,都从主内存中拿到最新的数据,而且可以防止指令重排,但volatile仅仅是保证变量的可见性,无法使数据加载的几个步骤是原子操作,所以volatile并不能保证线程安全。
如下代码所示:
多个业务线程访问用户余额balance,最终导致扣款总金额超过了用户余额,由线程不安全导致的资损情景.而且每个业务线程都扣款了两次,也说明了线程启动时需要将balance加载到工作内存中,之后该线程基于加载到的balance操作,其他线程如何改变balance值,对当前业务线程来说都是不可见的。
/** * 业务订单代扣线程 持续扣费 * User: lijinpeng * Created by Shanghai on 2019/4/13. */ @Slf4j public class WithHoldPayThread extends Thread { //缴费金额 private Integer amt; //业务类型 private String busiType; public WithHoldPayThread(Integer amt, String busiType) { this.amt = amt; this.busiType = busiType; } @Override public void run() { int payTime = 0; while (WithHodeTest.balance > 0) { synchronized (WithHodeTest.balance) { boolean result = false; if (WithHodeTest.balance >= amt) { WithHodeTest.balance -= amt; result = true; payTime++; } log.info("业务:{} 扣款金额:{} 扣款状态:{}", busiType, amt,result); } } log.info("业务:{} 共缴费:{} 次", busiType, payTime); } } 复制代码
测试函数
/** * User: lijinpeng * Created by Shanghai on 2019/4/13. */ public class WithHodeTest { //用户余额 单位 分 public static volatile Integer balance=100; public static void main(String[] args) { WithHoldPayThread phoneFare = new WithHoldPayThread(50, "缴存话费"); WithHoldPayThread waterFare = new WithHoldPayThread(50, "缴存水费"); WithHoldPayThread electricFare = new WithHoldPayThread(50, "缴存电费"); phoneFare.start(); waterFare.start(); electricFare.start(); } } 复制代码
执行结果:
实验结果证明,每个线程的扣款都成功了,这就导致了线程安全问题,解决这个问题最简单的做法是在run方法里面加synchronized修饰,并且对balance使用volatile修饰就可以了。
//用户余额 单位 分
public static volatile Integer balance=100;
复制代码
@Override
public void run() {
int payTime = 0;
while (WithHodeTest.balance > 0) {
synchronized (WithHodeTest.balance) {
boolean result = false;
if (WithHodeTest.balance >= amt) {
WithHodeTest.balance -= amt;
result = true;
payTime++;
}
log.info("业务:{} 扣款金额:{} 扣款状态:{}", busiType, amt,result);
}
}
log.info("业务:{} 共缴费:{} 次", busiType, payTime);
}
复制代码
执行结果: