了解多线程首先要了解进程和线程的概念,在操做系统里,进程是资源分配最小单位,通常状况下一个应用就会在计算机系统内开启一个进程,线程能够理解为进程中多个独立运行的子任务,是操做系统可以进行调度运算的最小单位,可是线程不拥有资源,只能共享进程中的数据,因此多个线程对进程中某个数据同时进行修改时,就会产生线程安全问题。java
因为一个进程中容许存在多个线程,因此在多线程中,如何处理线程并发和线程之间通讯的问题,是学习多线程编程的重点。 了解多线程首先要了解进程和线程的概念,在操做系统里,进程是资源分配最小单位,通常状况下一个应用就会在计算机系统内开启一个进程,线程能够理解为进程中多个独立运行的子任务,是操做系统可以进行调度运算的最小单位,可是线程不拥有资源,只能共享进程中的数据,因此多个线程对进程中某个数据同时进行修改时,就会产生线程安全问题。因为一个进程中容许存在多个线程,因此在多线程中,如何处理线程并发和线程之间通讯的问题,是学习多线程编程的重点。编程
在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()));
}
}
复制代码
如下是程序执行结果:多线程
结论:并发
Runable
和Callable
来讲,最后都是要借助于Thread开启线程,类是单继承的,接口是多实现的,因为生产环境业务复杂性,一个类可能会有其余功能,所以通常使用接口实现的方式。 一个线程的运行一般伴随着线程的启动、阻塞、中止等过程,线程启动能够经过Thread
类的start()
方法执行,因为多线程可能会共享进程数据,阻塞通常发生在等待其余线程释放进程某块资源的过程,当线程执行完毕,能够自动中止,也能够经过调用stop()
强制终止线程,或者在线程执行过程当中因为异常致使线程终止,了解线程的生命周期是学习多线程最重要的理论基础。ide
下图为线程的生命周期以及状态转换过程函数
当经过Thread thead=new Thread()
建立一个线程的时候,该线程就处于 new 状态,也叫新建状态。学习
就绪状态测试
当调用thread.start()
时,线程就进入了就绪状态,在该状态下线程并不会运行,只是表示线程进入可供CPU调用的就绪队列,具有运行条件。this
当线程得到了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
执行时机
read
和load
、store
和write
操做之一单独出现,以上两个操做必须按顺序执行,不必 连续执行,也就是说read
与load
之间、store
与write
之间是可插入其余指令的。assign
操做,即变量在工做内存中改变了以后必须把该变化同步回主内存。变量在当前线程中改变一次其实就是一次assign
,并且不容许丢弃最近的assign
,因此一定有一次store and write
,又根据第一条read and load 和store and write
不能单一出现,因此有一次store and write
一定有一次read and load
,所以推断出,变量在当前线程中每一次变化都会执行 read 、 load 、use 、assign、store、write
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);
}
复制代码
执行结果:
java线程类Thread提供了线程操做的基本方法,好比判断线程是否存活的isAlive()
,阻塞线程的wait() join()
,让线程休眠的sleep()
,中止线程的stop()
, 暂停和唤醒线程 的suspend
、resume
等等,有些方法因为会致使线程不安全或者独占资源已被废弃,因此咱们应该谨慎使用。
获取线程的惟一ID,此方法在实际生产中能够根据线程号跟踪一个业务的调用具体过程
判断线程是否处于活动状态,活动状态就是线程已启动,处于运行或者准备运行,还没有 结束,能够经过调用 isAlive()
方法判断当前线程任务是否执行完毕。
使正在执行任务的线程在指定的毫秒时间内暂停执行,正在执行的线程指this.currentThread()
返回的线程。该状态下的线程不会释放锁资源。
suspend()
能够暂停线程,resume
能够恢复线程继续执行。
/** * User: lijinpeng * Created by Shanghai on 2019/4/15. */
@Slf4j
public class SuspendAndResumeThread extends Thread {
@Getter
private int number = 0;
@Override
public void run() {
while (true) {
number++;
}
}
public static void main(String[] args) throws InterruptedException {
SuspendAndResumeThread thread=new SuspendAndResumeThread();
thread.start();
Thread.sleep(200);
thread.suspend();
//此时线程已经暂停执行
log.info("A time:{} number={}",System.currentTimeMillis(),thread.getNumber());
Thread.sleep(200);
//B time的执行结果应该与A time 一致
log.info("B time:{} number={}",System.currentTimeMillis(),thread.getNumber());
//唤醒继续执行
thread.resume();
Thread.sleep(200);
log.info("C time:{} number={}",System.currentTimeMillis(),thread.getNumber());
}
}
复制代码
执行结果和预期一致
suspend()
与resume()
能够暂停和恢复线程,用法也很简单,但是jdk却废弃了这种方法,由于这种用法可能会形成"资源独 占"的状况和数据不一致的状况。
资源独占:如下代码展现了多个支付业务,在同步代码中,支付业务withdraw在当前线程中暂停了线程执行,后面的支付业务没法在进入付款方法的状况,形成了资源独占
/** * User: lijinpeng * Created by Shanghai on 2019/4/17. */
@Slf4j
public class PaymentServiceImpl {
private int balance;
public PaymentServiceImpl(int balance) {
this.balance = balance;
}
public synchronized void payService(int amt) {
String buziType = Thread.currentThread().getName();
log.info("开始作支付业务.....业务类型:{}", buziType);
if (balance >= amt) {
balance -= amt;
//若是是提现业务 先去余额扣款 再作支付业务
if ("withdraw".equals(buziType)) {
log.info("转帐业务将当前线程 暂停!");
//暂停线程 模拟去转帐 若是转帐失败 该线程会一直处于在暂停 并且不会释放同步锁
Thread.currentThread().suspend();
log.info("转帐成功!");
}
log.info("业务:{} 扣款成功,当前用户余额:{}", buziType, balance);
} else {
log.info("业务:{} 扣款失败,当前用户余额不足", buziType);
}
}
public static void main(String[] args) throws InterruptedException {
final PaymentServiceImpl paymentService = new PaymentServiceImpl(2000);
Thread fastpay = new Thread(new Runnable() {
public void run() {
paymentService.payService(150);
}
}, "fastpay");
Thread withdraw = new Thread(new Runnable() {
public void run() {
paymentService.payService(50);
}
}, "withdraw");
Thread payfor = new Thread(new Runnable() {
public void run() {
paymentService.payService(50);
}
}, "payfor");
fastpay.start();
Thread.sleep(1000);
withdraw.start();
Thread.sleep(1000);
payfor.start();
}
}
复制代码
执行结果:
从执行结果上来看,fastpay
付款业务首先执行,不会受withdraw
业务暂停线程的影响,当执行withdraw
后,暂停了线程,若是在当前线程中不适用resume唤醒线程 会致使withdraw
线程一直占用payService()
方法资源。payfor
业务没法执行,产生资源独占的状况。
数据不一致:一下代码验证了经过ThreadExcuterUtils
获取当前线程名和执行任务,在两个参数赋值过程当中发生了线程暂停,致使线程名和执行任务不一致的状况。
/** * User: lijinpeng * Created by Shanghai on 2019/4/17. */
@Getter
@Slf4j
public class ThreadExcuterUtils {
//当前线程名
private String localThreadName;
//当前线程id
@Setter
private String localTask;
public void setThreadInfo(String threadName) {
this.localThreadName = threadName;
if ("gc_thread".equals(threadName)) {
Thread.currentThread().suspend();
}
}
public static void main(String[] args) throws InterruptedException {
final ThreadExcuterUtils utils = new ThreadExcuterUtils();
new Thread(new Runnable() {
public void run() {
utils.setThreadInfo("log_thread");
utils.setLocalTask("记录日志");
}
}).start();
Thread.sleep(1000);
new Thread(new Runnable() {
public void run() {
utils.setThreadInfo("gc_thread");
utils.setLocalTask("GC垃圾回收");
}
}).start();
Thread.sleep(1000);
log.info("当前线程:{} 当前任务:{}",utils.getLocalThreadName(),utils.getLocalTask());
}
}
复制代码
执行结果:
从执行结果看,当前线程是gc_thread
,对应的任务应该是 “GC垃圾回收”,可是结果倒是 “记录日志”,出现了数据不一致的状况。
以上两种状况可能因为业务场景设计的不合适或者能够用其余线程同步解决该问题,可是对于suspend
和resume
的使用,咱们应该谨慎当心,suspend
和resume
必定要成对使用,避免形成资源独占和数据不一致的状况。
在操做系统中,线程能够划分优先级,优先级较高的线程获得的CPU的资源越多,也就是CPU优先执行优先级较高的线程对象中的任务,设置线程优先级有助于帮助"线程规划器"肯定在下一次选择哪个线程来优先执行,设置线程优先级能够经过setPriority()
设置。在java中,线程优先级分为1-10 这个10个等级,数字越大优先级越高,若是小于1 或者大于10 会抛出throw new IlleageArgumentException()。
JDK使用3个常量预先设置线程优先级:
public final static int MIN_PRIORITY = 1;
/** * The default priority that is assigned to a thread. */
public final static int NORM_PRIORITY = 5;
/** * The maximum priority that a thread can have. */
public final static int MAX_PRIORITY = 10;
复制代码
一、线程优先级具备继承性
假如ThreadB extend ThreadA,ThreadA线程优先级为5,则ThreadB的优先级也为5
二、线程优先级具备随机性
设置线程的优先级,只能确保优先级高的尽量先得到cpu执行资源,可是并不表明着必定比优先级低的线程先得到执行权,具体仍是由cpu决定。
/** * User: lijinpeng * Created by Shanghai on 2019/4/17. */
@Slf4j
public class LowPriorityThread extends Thread {
@Override
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
log.info("☆☆☆☆☆");
}
}
/** * User: lijinpeng * Created by Shanghai on 2019/4/17. */
@Slf4j
public class HighPriorityThread extends Thread {
@Override
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
log.info("★★★★★");
}
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
HighPriorityThread thread = new HighPriorityThread();
LowPriorityThread thread1 = new LowPriorityThread();
thread.setPriority(10);
thread1.setPriority(1);
thread.start();
thread1.start();
}
}
复制代码
执行结果:
从执行结果看,优先级高的大部分线程老是会先于优先级的线程执行完。
yield()
表示当前线程可让出cpu资源,cpu资源是否让出取决于cpu调度,当cpu资源紧张时候可能会收回该线程执行资源,若是cpu资源充足,有可能就不会回收。使用了yield()
该线程任务时间可能会延长。
如下代码验证了不适用yeild和使用yeild线程执行时间对比:
/** * User: lijinpeng * Created by Shanghai on 2019/4/17. */
@Slf4j
public class YieldThread extends Thread {
@Override
public void run() {
// Thread.yield();
long beginTime = System.currentTimeMillis();
int result = 0;
for (int i = 0; i < 500000000; i++) {
result += i;
}
long endTime = System.currentTimeMillis();
log.info("当前线程执行时间:{} 毫秒",endTime-beginTime);
}
public static void main(String[] args) {
YieldThread thread=new YieldThread();
thread.start();
}
}
复制代码
如下是两次执行结果对比,会发现使用了yiled()
执行方法有所延长,当在CPU资源紧张时,两个执行耗时差异会更明显。
不使用yeild
使用了yeild
中止一个线程意味着中止线程正在作的任务,放弃当前操做,可是中止线程操做并不像java种的for循环使用break退出循环这么简单,由于线程执行任务过程可能会使用到主内存中的共享数据,一旦放弃该线程任务,其所操做的共享数据处理不当就会产生非线程安全问题。中止一个正在执行中的线程可使用Thread.stop()
方法,直接暴力,不过stop
方法和suspend
和resume
同样,都是过时方法,会产生不可预料的结果,目前比较安全的方法是使用Thread.interrupt()
,在线程中使用该方法会给线程打一个中止的标志,并不会真正的中止,可是咱们能够经过判断是否存在这个标志点在run()进行任务终止。总结一下,java中有三种中止线程的方法:
stop
强制终止线程,若是使用共享资源,会产生不可预料的结果,已废弃。interrupt()
终端线程。//会清除线程状态
public static boolean interrupted() //不会清楚线程状态 public boolean isInterrupted() 复制代码
第一个方法interrupted()
是静态的,用来判断执行这段代码的线程是否处于终止状态,调用该方法会清除当前线程状态,好比第一次调用时获取线程状态时true,以后清除了该线程状态,第二次调用就会变成false。(用的不多)
第二个方法属于该线程对象,用于判断当前线程是否处于终止状态,该方法不会清除线程状态,因此若是线程状态不变,屡次调用该方法获取的线程状态必定时一致的。(很是经常使用)
咱们能够经过thread.isInterrupted()
来获取线程状态,在线程执行过程当中,能够经过判断线程状态来决定线程是否要继续执行,若是不须要执行能够直接抛出异常 或者使用return直接返回 终止异常 就能够达到终止线程的目的。
以下代码所示:
线程类每间隔1s打印一下当前时间,每次打印前先判断线程状态,若是线程未终止继续执行,若是终止就抛出异常!
/** * User: lijinpeng * Created by Shanghai on 2019/4/17. */
@Slf4j
public class LogRecordThread extends Thread {
@Override
public void run() {
try {
while (true) {
if (!this.isInterrupted()) {
Thread.sleep(1000);
log.info("日志记录工做执行中!当前时间:{}", System.currentTimeMillis());
} else {
throw new InterruptedException();
}
}
} catch (InterruptedException ex) {
log.error("当前线程已终止,任务终止!");
// ex.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
LogRecordThread thread = new LogRecordThread();
thread.start();
Thread.sleep(5000);
thread.interrupt();
}
}
复制代码
运行结果:
如上所示线程跟预期同样结束了。此外还可使用return直接返回便可结束线程任务。
若是线程处于sleep状态中,调用thread.interrupt()
方法会抛出InterruptedException
异常,此外还会清除中止状态值,使之变成false,能够简单理解为,若是在线程sleep的时候调用thread.interrupt()
时候,线程状态值与以前保持一致。
/** * User: lijinpeng * Created by Shanghai on 2019/4/17. */
@Slf4j
public class SleepInterruptThread extends Thread {
@Override
public void run() {
try {
log.info("开始执行线程任务....");
Thread.sleep(3000);
log.info("线程任务执行完毕!");
} catch (InterruptedException ex) {
log.error("线程被终止了,此时线程是否中断标志:{}",this.isInterrupted());
ex.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
SleepInterruptThread thread=new SleepInterruptThread();
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
复制代码
运行结果:
直接调用thread.stop就能够中止一个线程,可是因为中止线程会致使一些处理工做没法完成,致使数据不完整,还会和suspend和resume同样产生数据不一致的状况,因此jdk已经不建议使用。若是想要中止线程直接经过上面的异常法便可。
本文主要介绍了线程的三种建立方式,线程的几种状态以及状态转换、线程安全问题 、线程基本API以及如何中止一个线程,学习掌握了这些其实也就掌握了多线程的核心,虽然线程并发安全问题是咱们关注的重点,但不了解上面几点就没法真正理解产生以及解决线程安全问题的方法,虽然简单,仍是要记录一下。