发布于2019-08-30 11:04 阅读(726) 评论(0) 点赞(7) 收藏(1)
了解多线程首先要了解进程和线程的概念,在操作系统里,进程是资源分配最小单位,一般情况下一个应用就会在计算机系统内开启一个进程,线程可以理解为进程中多个独立运行的子任务,是操作系统能够进行调度运算的最小单位,但是线程不拥有资源,只能共享进程中的数据,所以多个线程对进程中某个数据同时进行修改时,就会产生线程安全问题。由于一个进程中允许存在多个线程,所以在多线程中,如何处理线程并发和线程之间通信的问题,是学习多线程编程的重点。
复制代码
在java中,创建一个线程一般有两种方式,继承Thread类或者实现Runable接口,重写run方法即可,然后调用start()方法即可以开启一个线程并执行。如果想要获取当前线程执行返回值,在jdk1.5以后,可以通过实现Callable接口,然后借助FutureTask或者线程池得到返回值。由于线程的执行具有随机性,所以线程的开启顺序并不意味线程的执行顺序。
/**
* 继承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());
}
}
复制代码
/**
* 实现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());
}
}
复制代码
/**
* 实现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;
}
}
复制代码
/**
* 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类的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);
}
复制代码
执行结果:
作者:听爸爸的话
链接:https://www.pythonheidong.com/blog/article/71006/8ab4ea8e8c678b0d9a07/
来源:python黑洞网
任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任
昵称:
评论内容:(最多支持255个字符)
---无人问津也好,技不如人也罢,你都要试着安静下来,去做自己该做的事,而不是让内心的烦躁、焦虑,坏掉你本来就不多的热情和定力
Copyright © 2018-2021 python黑洞网 All Rights Reserved 版权所有,并保留所有权利。 京ICP备18063182号-1
投诉与举报,广告合作请联系vgs_info@163.com或QQ3083709327
免责声明:网站文章均由用户上传,仅供读者学习交流使用,禁止用做商业用途。若文章涉及色情,反动,侵权等违法信息,请向我们举报,一经核实我们会立即删除!