为什么Java只讨论多线程,而不讨论多进程?
进程是对操作系统而言的,而线程是对程序而言的,Java代表的是程序,每次运行一个Java程序就相当于一个进程。
线程的创建和启动
Java使用Thread类代表线程,所有线程对象都是Thread类或子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流。
继承Thread类创建线程类
步骤如下:
- 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务。因此把run()方法成为线程执行体;
- 创建Thread子类的实例,即创建线程对象;
- 调用线程对象的start方法来启动该线程;
package org.westos.demo01;
public class FirstThread extends Thread{
//重写run方法,run方法的方法体就是线程执行体
@Override
public void run() {
for (int i=0;i<100;i++){
//getName()获取当前线程的线程名,省略了this
//也可以使用类方法Thread.currentThread().getName()来获取线程名
System.out.println(getName()+"-->"+i);
// System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
public static void main(String[] args) {
//创建两个线程对象
FirstThread th1 = new FirstThread();
FirstThread th2 = new FirstThread();
//给线程设置名字
th1.setName("java");
th2.setName("python");
//开启线程
th1.start();
th2.start();
}
}
可以看到两个线程来回切换执行,不过是随机的。
虽然上面程序只显示两个线程,但实际上程序有三个线程,程序显式创建的2个子线程和主线程。当java程序开始运行之后,程序至少会创建一个主线程。主线程的执行体不是run方法确定的,而是由main()方法确定的。
实现Runable接口创建线程类
实现Runnable接口来创建并启动多线程的步骤如下:
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法同样是该线程的线程执行体。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
package org.westos.demo01;
public class RunThread implements Runnable{
@Override
public void run() {
for (int i=0;i<100;i++){
//这里只能使用Thread.currentThread().getName()而不能使用this对象调用
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
public static void main(String[] args) {
RunThread rt = new RunThread();
//通过
new Thread(rt,"新线程1").start();
new Thread(rt,"新线程2").start();
}
}
Runnable接口中只有一个抽象方法,所以它是函数式接口,可以使用Lambda表达式来创建Runnable对象。
package org.westos.demo01;
public class RunThread {
public static void main(String[] args) {
new Thread(()-> {
for (int i=0;i<100;i++){
//getName()获取当前线程的线程名,省略了this
//也可以使用类方法Thread.currentThread().getName()来获取线程名
// System.out.println(getName()+"-->"+i);
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}).start();
}
}
使用Callable和Future创建对象
从Java5开始,Java提供了一个Callable接口,该接口提供了一个call方法作为线程执行提,该方法比run方法功能更强大。
- call方法可以有返回值;
- call方法可以声明抛出异常;
java5提供了Future接口来代表Callable接口里的call方法的返回值,并为Future接口提供一个FutureTask实现类,该实现类实现了Future接口,并实现了Runnable接口,可作为Thread类的target。
有如下方法来控制它关联的Callable任务:
boolean cancel(boolean mayInterruptIfRunning)
:试图取消该Future里关联的Callable任务;V get()
:返回Callable任务里call方法的返回值,调用该方法将造成线程阻塞,必须等到子线程结束后才会得到返回值。V get(long timeout,TimeUnit unit)
:返回Callable任务里call方法的返回值,该方法让程序最多阻塞timeout和unit指定的时间,如果经过指定时间后Callable任务依然没有返回值,将会抛出TimeoutException异常。boolean isCancelled()
:如果在Callable任务正常完成前被取消,则返回true;boolean isDone()
:如果Callable任务已完成,则返回true。
创建启动有返回值线程的步骤:
- 创建Callable接口的实现类,并实现call方法,该call方法将作为线程执行体,且该call方法有返回值,在创建Callable实现类的实例。从java8开始可以直接使用Lambda表达式创建callable对象;
- 使用FutureTask类来包装Callable对象,该Future对象封装了该Callable对象的call方法的返回值;
- 使用功FutureTask对象作为Thread对象的target创建并启动新线程;
- 调用功FutureTask对象的get()方法来实现线程类,并启动该线程;
package org.westos.demo01;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.function.Function;
public class ThirdThread {
public static void main(String[] args) {
//创建Callable对象
ThirdThread rt = new ThirdThread();
//使用Lambda表达式创建Callable<Integer>对象
FutureTask task = new FutureTask<Integer>((Callable<Integer>)()->{
int i=0;
for (;i<100;i++){
System.out.println(Thread.currentThread().getName()+"的循环变量i的值"+i);
}
return i;
});
for (int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+"循环变量i的值"+i);
if(i==20){
new Thread(task,"有返回值的线程").start();
}
}
//获取线程返回值
try{
System.out.println("子线程的返回值"+task.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
创建线程的三种方式对比
Runnable和Callable方式基本类似,所以归为一种。
采用Runnable和Callable接口的方式创建的优缺点:
- 线程类只是实现了接口,还可以继承其他类;
- 在这种方式下多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,较好体先面向对象的思想。
- 缺点是,编程比较复杂;
采用Thread类的方式创建多线程的优缺点:
- 确定是,不能在继承别的类;
- 优势,编写简单,可以直接使用this获得当前线程对象。
线程生命周期
线程的生命周期中它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。
线程的状态
- 新建:新建的线程对象和其他java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值;
- 就绪:当线程对象调用start()方法后,线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态的线程表示可以运行了,只是在等待执行权限。取决于JVM里线程调度器的调度。
- 运行:当就绪状态的线程获得了CPU,开始执行run方法的现成的执行体,则该线程处于运行状态。当一个线程开始运行之后,他不一定一直处于运行状态,对于采用抢占式策略的系统而言,系统会给每个可执行的线程一小时间段来处理任务。
- 阻塞:被阻塞的线程就没有执行权限了,会在合适的时候重新进入就绪状态。
- 死亡:线程被结束;
线程被阻塞的原因
- 线程调用sleep()方法主动放弃所占用的处理器资源;
- 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞;
- 线程试图获得一个同步监视器,但该同步监视器正在被其他线程所持有;
- 线程正在等待某个通知(notify);
- 程序调用了线程的suspend()方法将该线程挂起。
解除阻塞的方法
解除阻塞让线程重新进入就绪状态;
- 调用sleep()方法的线程经过了指定时间;
- 线程调用的阻塞式IO方法已经返回;
- 线程成功地获得了试图取得的同步监视器;
- 线程获得了正在等待的通知;
- 出于挂起状态的线程被调用了resume恢复方法;
线程怎么死亡
- run()或call()方法执行完成,线程正常结束;
- 线程抛出一个未捕获的异常;
- 调用stop()方法(容易造成死锁)。
注意
- 主线程结束时,其他线程不受任何影响,并不会随之结束,一旦子线程启动起来后,它就拥有和主线程相同的地位,不会受主线程的影响。
- 不要试图对一个死亡的线程调用start方法让它重新就绪。
- 不要对新建的线程调用两个或多次start()方法。
控制线程
join线程
Thread提供了让一个线程等待另一个线程完成的方法——join方法,当在某个程序执行流中调用其他线程的join方法时,调用线程将被阻塞,直到被join方法加入的join线程执行完为止。
package org.westos.demo01;
public class JoinThreadTest {
static int i=0;
public static void main(String[] args) throws InterruptedException {
for (;i<100;i++){
//获取主线程名字
System.out.println(Thread.currentThread().getName()+"--->"+i);
//当i=20时创建子线程
if(i==20){
//创建子线程
Thread sonThread = new Thread(new Runnable() {
@Override
public void run() {
for (int i1 = 0; i1 < 100; i1++) {
//获取子线程
System.out.println(Thread.currentThread().getName()+"--->"+i1);
//当i为30时,将子线程
}
}
},"sonThread");
sonThread.start();
//将sonThread加入到join,直到子线程执行完,主线程才开始继续执行
sonThread.join();
}
}
}
}
当主线程循环到20时创建子线程,启动并且join到执行流,此时主线程开始阻塞,直到子线程执行完,主线程继续开始从21执行。
join方法的三种重载形式:
join()
:等待被join的线程执行完成;join(long millis)
:等待被join的线程的时间最长为millis毫秒。如果到时候还没有结束则不再等待。
后台线程
也成为守护线程或精灵线程,他是在后台运行的,如果所有前台线程都死亡,后台线程会自动死亡。
调用Thread对象的setDaemon(true)方法可将指定线程设置成后台线程。
实例:将执行线程设置为后台线程,可以看到当所有前台线程死亡时,后台线程随之死亡。
package org.westos.demo01;
public class DaemonThread {
public static void main(String[] args) {
Thread dt=new Thread(()->{
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
},"DaemonThread");
//设置为守护线程
dt.setDaemon(true);
dt.start();
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
//当主线程(前台线程运行完)程序也随之结束,守护线程也结束
}
}
注意:
前台线程死亡后,JVM会通知后台线程死亡,但从它接受指令到做出响应,需要一定时间,而且要将某个线程设置为后台线程,必须在该线程启动之前设置,否则会引发IllegalThreadStateException异常。
线程睡眠:sleep
如果需要让当前执行正在执行的线程暂停一段时间,并进入阻塞状态,则可用Thread类的静态方法sleep()来实现。
static void sleep(long millis)
:让当前正在执行的线程暂停millis毫秒,并进入阻塞状态。在睡眠时间段,该线程不会获得任何执行的机会,即使系统中已经没有其他可执行的线程。
线程让步:yield
yield()方法和sleep方法类似。也是静态方法,也可以让当前正在执行的线程暂停,但它不会阻塞该线程,只是将线程转入就绪状态。事实上,当某个线程调用了yield方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态的线程才会获得执行的机会。
package org.westos.demo01;
public class YieldThread extends Thread{
public YieldThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(getName()+" "+i);
if(i==20){
Thread.yield();
}
}
}
public static void main(String[] args) {
YieldThread th1 = new YieldThread("th1");
YieldThread th2 = new YieldThread("th2");
th1.setPriority(Thread.MAX_PRIORITY);
th1.start();
th2.setPriority(Thread.MIN_PRIORITY);
th2.start();
}
}
sleep和yield的区别
- sleep不会理会线程的优先级,yield会有优先级;
- sleep将线程转入阻塞状态,yield将线程转入就绪状态;
- sleep方法要抛出InterruptedException异常,而yield不抛出;
- sleep比yield具有更好的可移植性。
改变线程的优先级
每个线程执行时都具有一定的优先级,优先级高的获得较多的执行机会。没个线程默认的优先级与创建它的父线程相同。默认情况下main线程和它创建的子线程都具有普通优先级。
Thread类提供了setPriority(int newPriority),getPriority方法来设置和返回执行线程的优先级。其中setPriority方法的参数可以是一个整数,范围是1~10之间。也可以用静态常量来表示:
- MAX_PRIORITY:10
- MIN_PRIORITY:1
- NORM_PRIORITY:5
package org.westos.demo01;
import java.util.TreeMap;
public class PriorityTest extends Thread{
public PriorityTest(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(getName()+"其优先级:"+getPriority()+",循环变量的值:"+i);
}
}
public static void main(String[] args) {
//改变主线程的优先级
Thread.currentThread().setPriority(6);
for (int i = 0; i < 30; i++) {
System.out.println("主线程优先级:"+Thread.currentThread().getPriority()+",循环变量的值:"+i);
if(i==10){
PriorityTest low = new PriorityTest("低级");
low.start();
System.out.println("低级创建之初的优先级:"+low.getPriority());
//设置该线程优先级为最低
low.setPriority(Thread.MIN_PRIORITY);
}
if(i==20){
PriorityTest high = new PriorityTest("高级");
high.start();
System.out.println("高级创建之初的优先级:"+high.getPriority());
//设置该线程优先级为最高
high.setPriority(Thread.MAX_PRIORITY);
}
}
}
}
优先级高的线程具有更改概率的执行权。
线程同步
线程安全问题
模拟售票问题,一共有100张票,三个窗口来销售这100张票;
package org.westos.demo93;
public class TicketSaledDemo implements Runnable {
//定义票数
int ticket=100;
//售票执行体
@Override
public void run() {
//模拟网络延迟
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
while (true){
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"正在售第"+ticket--+"张票");
}
}
}
}
package org.westos.demo93;
public class TicketTest {
public static void main(String[] args) {
TicketSaledDemo ticket = new TicketSaledDemo();
//创建三个窗口(线程)
Thread win1 = new Thread(ticket, "窗口1");
Thread win2 = new Thread(ticket, "窗口2");
Thread win3 = new Thread(ticket, "窗口3");
win1.start();
win2.start();
win3.start();
}
}
出现了下面两个问题:
- 出现相同票数
- 出现负票数
运行结果出现了线程安全问题,线程安全问题的条件:
- 多线程环境
- 要有共享共享数据
- 有多条语句操作共享数据
先说出现相同票数的原因,这是因为代码中的 ticket--
不是原子操作,那什么是原子操作呢?原子操作就是只有一条语句的操作,而事实上 ticket--
是分为三步进行操作的。可以分为:
//存原值
int t=ticket;
//运算
t=ticket-1;
//刷新旧值
ticket=t;
所以个代码不是原子操作,这就有可能导致了win1线程拿到了ticket=9时,还没来得及进行减1,win2线程就开始执行了,win2线程拿到的ticket值也是9,所以就导致了最后得到了相同的票数。
出现负票的原因,这是由于线程的随机性导致的,代码中if(ticket>0)
,线程的切换是随机的,比如当ticket=1时,win1线程先进入到if语句中,还没来得及执行减1,此时CPU的执行权限就被Win2线程抢去了,这时ticket还是1,所以win2也能进入if语句,这两个线程都进入到了然后不管谁先执行减1,最后两个线程都会执行,所以就减了2,最后结果得到了第-1张票。
出现问题
出现这两个问题的都是因为run方法的执行体不具有同步性,因为有可能多个线程并发执行run方法,就导致了线程不安全。
解决方法:同步代码块
为了解决这个问题,Java的多线程支持引入了同步监视器,使用同步监视器的通用方法就是同步代码块,格式如下:
synchronized(obj){
//要同步的代码块
}
java程序允许任何对象作为同步监视器,同步监视器的目的:阻止两个线程对同一个共享资源进行并发访问,通常推荐使用可能并发访问的共享资源充当同步监视器。
package org.westos.demo93;
public class TicketSaledDemo implements Runnable {
static Object obj=new Object();
//定义票数
static int ticket=100;
//售票执行体
@Override
public void run() {
while (true){
synchronized (obj){
if(ticket>0){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在售第"+ticket--+"张票");
}
}
//同步代码块结束,线程释放锁
}
}
}
想到于加锁——>票数减1——>释放锁,任何线程在修改指定资源之前,首先对该资源进行加锁,在加锁到释放锁期间,其他线程无法修改该资源,当修改完成之后,释放锁。通过这种方式就可以保证在并发过程中任意时刻只有一个线程可以进入同步代码块。
同步方法
与同步代码块对应Java的多线程安全还包含了同步方法,同步方法就是使用synchronized关键字来修饰某个方法,则称为同步方法。同步方法无需显式指同步监视器,默认为this。
package org.westos.demo93;
public class TicketSaledDemo implements Runnable {
static Object obj=new Object();
//定义票数
static int ticket=100;
//售票执行体
@Override
public void run() {
while (true){
excute();
//同步代码块结束,线程释放锁
}
}
private synchronized void excute() {
if(ticket>0){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在售第"+ticket--+"张票");
}
}
}
线程安全是以降低程序的运行效率作为代价,为了减少线程安全所带来的负面影响可采用如下策略:
- 不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源的方法进行同步。
- 如果可变类有两种运行环境,单线程和多线程,则应该为该可变类提供两个版本,即线程不安全和线程安全。在单线程中使用线程不安全版本以保证性能,在多线程环境使用线程安全版本。
释放同步监视器的锁定
- 当前线程的同步方法、同步代码块执行结束,释放当前线程的同步监视器;
- 当前线程在同步代码块遇到break、return终止了该代码块、该方法的继续执行,释放。
- 当前线程在同步代码块遇到未处理的Error,Exception导致该代码块、该方法的异常结束,释放。
- 当前线程在同步代码块遇到了同步监视器对象的wait()方法,则当前线程暂停,释放。
如下情况不会释放:
- 在同步代码块遇到Thread.sleep()、Thread.yield()、当前线程的suspend(),不会释放同步监视器;
同步锁(Lock)
从Java5开始,Java提供了一种功能更强大的线程同步机制——通过显式定义同步锁对象来实现同步,同步锁由Lock对象充当。
在实现线性安全的控制中,比较常用的是ReentranLock(可重入锁),使用该对象可以显式地加锁、释放锁。
package org.westos.demo93;
import java.util.concurrent.locks.ReentrantLock;
public class TicketSaledDemo implements Runnable {
//定义锁对象
private final ReentrantLock lock=new ReentrantLock();
//定义票数
static int ticket=100;
//售票执行体
@Override
public void run() {
while (true){
excute();
}
}
//需要同步的方法
private void excute() {
//加锁
lock.lock();
if(ticket>0){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
finally {
System.out.println(Thread.currentThread().getName()+"正在售第"+ticket--+"张票");
//释放锁
lock.unlock();
}
}
}
}
使用ReentranLock对象来进行同步,加锁和释放锁出现在不同的作用范围时,通常建议使用finally块来确保在必要时候释放锁。
ReentranLock锁具有可重入性,一个线程可以对已被加锁的ReentranLock锁再次加锁,ReentranLock对象会为之一个计数器来追踪lock()方法的嵌套调用,线程在每次调用lock()加锁后,必须显式调用unlock来释放锁,所以一段被锁保护的代码可以调用两一个被相同锁保护的方法。
死锁
先来看一个案例:
package org.westos.demo6;
public class ObjUtils {
//创建两个同步线程锁
public static final Object objA=new Object();
public static final Object objB=new Object();
}
package org.westos.demo6;
import java.util.concurrent.locks.ReentrantLock;
public class LockTest extends Thread{
private boolean b;
public LockTest(boolean b) {
this.b=b;
}
@Override
public void run() {
if(b){
synchronized (ObjUtils.objA){
System.out.println("trueA进来了");
synchronized (ObjUtils.objB){
System.out.println("trueB进来了");
}
}
}else {
synchronized (ObjUtils.objB){
System.out.println("falseB进来了");
synchronized (ObjUtils.objA){
System.out.println("falseA进来了");
}
}
}
}
public static void main(String[] args) {
LockTest lockA = new LockTest(true);
LockTest lockB = new LockTest(false);
lockA.start();
lockB.start();
}
}
这种现象称为死锁,两个线程执行体都需要两个锁才能执行完,而当第一个线程的拿到了A锁时,第二个线程也拿到了B锁,而第一个线程还需要B锁,第二个线程还需要A锁才能执行,由于执行体还没有执行完,两个线程都不会释放锁,就陷入了僵持的状态。