1、什么是多线程
得益于计算机的时间片机制,每一个应用程序的都可以在一段很小的时间段内执行。相比于单线程串行执行,得不到时间片就停止执行,多线程当中线程1得不到时间片,线程2有可能得到,可以更多的完成任务。
还有一种场景,单线程要操作IO设备,但是IO设备一般都会有较多的等待,这段时间CPU处于空闲,空闲的CPU资源就被浪费了,如果能把资源让给另外一个需要CPU资源的线程,那就提高了任务完成的速率,让活干的更快。
线程:拿微信举例,微信相当于一个进程,在微信启动后,向操作系统索要了一些资源的使用权限,线程相当于微信里面的打工人,这些打工人有很多岗位,比如收发消息岗、转账岗、抢红包岗等等,这些具体的工作,微信创建出来很多线程去干。怎么样让这些打打工人又快又好的干活呢,接着往下看。
2、JAVA多线程使用
2.1、创建线程
2.1.1、继承Thread类
Thread类是JVM中用于管理线程的一个类,每一个线程都有唯一的一个Thread对象与之一一对应
package thread;
public class TestThread extends Thread {
@Override
public void run() {
System.out.println("我是一个线程,我的名字是:"+Thread.currentThread().getName());
}
public static void main(String[] args) throws InterruptedException {
TestThread testThread = new TestThread();
testThread.start();
testThread.join();
}
}
2.1.1.1、Thread类及常见方法
常见构造方法
方法 | 说明 |
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target, String name) | 使用 Runnable 对象创建线程对象,并命名 |
Thread(ThreadGroup group, Runnable target) | 线程可以被用来分组管理,分好的组即使线程组 |
常见属性
属性 | 获取方法 |
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
2.1.2、实现Runnable函数式接口
自JAVA诞生,Runnable就已经存在了,如果一个线程类要实现 Runnable
接口,则这个类必须定义一个名为 run
的无参数方法。
实现了 Runnable
接口的类可以通过实例化一个 Thread
实例,并将自身作为目标传递来运行。
Runnable定义:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
Runnable使用举例:
package thread;
public class TestThread implements Runnable {
@Override
public void run() {
System.out.println("我是一个线程,我的名字是:" + Thread.currentThread().getName());
}
public static void main(String[] args) throws InterruptedException {
TestThread testThread = new TestThread();
Thread thread = new Thread(testThread);
thread.start();
thread.join();
}
}
函数式写法,这是JDK1.8之后,对Runnable加上了@FunctionalInterface函数式接口的修饰,可以实现函数式编程,也叫Lambda表达式,想了解Lambda表达式可以参考Lambda、函数式接口、Stream 一次性全给你
package thread;
public class TestThread {
public static void main(String[] args) {
Runnable runnable = ()->{
System.out.println("我是一个线程,我的名字是" + Thread.currentThread().getName());
};
Thread thread = new Thread(runnable);
thread.start();
}
}
注意,是调用新 new 出来的 Thread 实例的start()
方法,不要调用run
方法,虽然我们是重写Runnable
的 run
方法的。调用 run
方法并没有创建线程的效果,而是直接在当前线程执行,就和执行一个普通类的普通方法一模一样。
为什么要调用 start()
方法呢,我们看看 Thread
的 start()
方法实现中,其实是调用了一个名称为 start0()
的 native 方法,native 方法就不是用 Java 实现的了,而是在 JVM 层面的实现。
这个start0
方法的主要逻辑就是启动一个操作系统线程,并和 JVM 线程绑定,开辟一些空间来存储线程状态和上下文的数据,然后执行绑定的 JVM 线程(也就是我们实现了Runnable的类)的 run
方法的代码块,从而执行我们自定义的逻辑。
其实Runnable并不是很完美,其不能返回值,不能抛出异常,所以有了Callable,来实现这些功能。
2.1.3、实现Callable函数式接口
Callable定义
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
Callable使用举例:
package thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
public class TestCallable {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> callable = () -> {
int sum = 0;
for (int i = 0; i < 5; ++i) {
sum += i;
//TimeUnit.SECONDS.sleep(1);
throw new Exception("业务出现异常");
}
return sum;
};
FutureTask<Integer> integerFutureTask = new FutureTask<>(callable);
Thread thread = new Thread(integerFutureTask);
thread.start();
System.out.println("我是main线程");
Integer result = null;
try {
result = integerFutureTask.get();
} catch (InterruptedException e) {
e.printStackTrace();
}catch (Exception e) {
System.out.println("处理异常:"+e.getMessage());
e.printStackTrace();
}
System.out.println("最终运行结果为:"+result);
}
}
2.1.4、Thread、Runnable、Callable之间的区别
本质上,Runnable和Callable只是Thread要执行的任务类,并且Runnable和Callable是函数式接口。在java中一个类只能继承一个父类,可以实现多个接口,Runnable和Callable方式的好处是可以规避类的单继承的限制;
Runnable和Callable之间的区别:
Callable实现的是call()方法,Runnable实现的是run()方法
Callable可以通过FutureTask对象获取返回值,而Runnable则没有返回值
Callable可以抛出异常,而Runnable不支持
Callable要搭配FutureTask一起使用
2.2、线程控制
2.2.1、中断一个线程
常见的有以下两种方式:
1、通过共享的标记来沟通
2、调用interrupe()方法来通知
示例1:
package thread;
public class ThreadDemo {
private static class MyRunnable implements Runnable {
public volatile boolean isQuit = false;
@Override
public void run() {
while (!isQuit) {
System.out.println(Thread.currentThread().getName()
+ ": 别管我,我忙着转账呢!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()
+ ": 啊!险些误了大事");
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
System.out.println(Thread.currentThread().getName()
+ ": 让李四开始转账。");
thread.start();
Thread.sleep(10 * 1000);
System.out.println(Thread.currentThread().getName()
+ ": 老板来电话了,得赶紧通知李四对方是个骗子!");
target.isQuit = true;
}
}
示例2:
package thread;
public class ThreadDemo {
private static class MyRunnable implements Runnable {
@Override
public void run() {
// 两种方法均可以
// while (!Thread.interrupted()) {
while (!Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName()
+ ": 别管我,我忙着转账呢!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println(Thread.currentThread().getName()
+ ": 有内鬼,终止交易!");
break;
}
}
System.out.println(Thread.currentThread().getName()
+ ": 啊!险些误了大事");
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
System.out.println(Thread.currentThread().getName()
+ ": 让李四开始转账。");
thread.start();
Thread.sleep(10 * 1000);
System.out.println(Thread.currentThread().getName()
+ ": 老板来电话了,得赶紧通知李四对方是个骗子!");
thread.interrupt();
}
}
重点说明下第二种方法:
1、通过 thread 对象调用 interrupt() 方法通知该线程停止运行
2、thread 收到通知的方式有两种:
1、如果线程调用了wait()/join()/sleep()方法而阻塞挂起,则以InterruptedException异常的方式通知,清楚中断标志。
2、否则,只是内部的中断标识被设置,需要手动判断,在判断上也有区别;thread.interrupted()判断当前线程的中断标志,清除中断标志;thread.currentThread().isInterruptd(),判断指定线程的中断标识是否被限制,不清除中断标志。
方法 | 说明 |
public void interrupt() | 中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标志位 |
public static boolean interrupted() | 判断当前线程的中断标志位是否设置,调用后清除标志位 |
public boolean isInterrupted() | 判断对象关联的线程的标志位是否设置,调用后不清除标志位 |
2.2.2、等待一个线程join
package thread;
import java.util.concurrent.atomic.AtomicInteger;
public class TestJoin {
public static void main(String[] args) throws InterruptedException {
//原子整型
AtomicInteger atomicInteger = new AtomicInteger();
Thread thread = new Thread(() -> {
//五万次自增操作
for (int i = 0; i < 50000; i++) {
atomicInteger.getAndIncrement();
}
});
Thread thread2 = new Thread(() -> {
//五万次自增操作
for (int i = 0; i < 50000; i++) {
atomicInteger.getAndIncrement();
}
});
//启动线程
thread.start();
thread2.start();
//等待两个线程执行完成
thread.join();
thread2.join();
System.out.println(atomicInteger);
}
}
想要得到线程的执行结果,就要等待线程执行完毕,join方法可以等待线程执行完毕,这里值得说明的是:Java 中的多线程代码不会随着主线程的退出而退出,在JAVA中,所有的线程都是通过线程对象来创建和管理的,点一个JAVA应用程序启动时,至少有一个主线程在运行,当主线程启动并创建其他线程时,这些线程就成了独立的执行流,可以在主线程退出之后,继续执行,每个线程都有自己的生命周期,他们独立于主线程的运行状态。
与 Java 不同,C++ 中的多线程代码会随着主线程的退出而退出,这是因为 C++ 标准库对线程的处理方式与 Java 不同。C++ 的线程是基于操作系统的线程模型实现的,而不是像 Java 那样由虚拟机管理。当主线程退出时,C++ 运行时库会通知操作系统关闭所有线程。主线程退出意味着整个进程退出,所有线程都会被终止。
需要注意的是,无论是在 Java 还是在 C++ 中,如果某个线程持有资源(如文件句柄、数据库连接等),而其他线程仍在使用这些资源,那么这些资源并不会自动释放。这需要开发者在编写代码时正确管理资源,并确保适当地关闭或释放资源。
方法 | 说明 |
public void join() | 等待线程结束 |
public void join(long millis) | 等待线程结束,最多等 millis 毫秒 |
public void join(long millis, int nanos) | 同理,但可以更高精度 |
2.2.3、休眠当前线程
2.2.3.1、sleep
方法 | 说明 |
public static void sleep(long millis) throws InterruptedException | 休眠当前线程 millis 毫秒 |
public static void sleep(long millis, int nanos) throws InterruptedException | 可以更高精度的休眠 |
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println(System.currentTimeMillis());
Thread.sleep(3 * 1000);
System.out.println(System.currentTimeMillis());
}
}
2.2.3.2、wait
wait方法使当前线程停止运行。
1、wait()方法是Object类的方法,该方法是将当前线程放入到线程就绪队列中,在wait()方法之后停止运行,直到接到通知或者被中断为止。
2、wait()方法只能在同步方法中或同步代码块中调用,如果调用是没有适当的锁,或抛出异常。
3、wait()方法调用后,当前线程释放锁,知道其他线程调用此线程对象的notify()方法或是notifyAll()方法,当前线程被唤醒进入就绪状态。
4、wait((long timeout))让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的notify()方法或 notifyAll() 方法, 或者超过指定的时间量”,当前线程被唤醒(进入“就绪状态”)。
2.2.3.3、notify方法
使被wait的线程继续运行。
1、方法notify()也要在同步方法或者是同步代码块中被调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。如果有多个线程等待,则有线程规划器随机挑选出一个呈wait状态的线程。
2、在执行完notify()方法之后,当前线程不会马上释放锁,而是要等待当前线程执行完notify()之后的代码,也就是退出同步代码块之后再释放锁。
package thread;
public class TestNotify implements Runnable {
private boolean flag;
private Object obj;
public TestNotify(boolean flag, Object obj) {
super();
this.flag = flag;
this.obj = obj;
}
public void waitMethod() {
synchronized (obj) {
try {
while (true) {
System.out.println("wait()方法开始.. " +
Thread.currentThread().getName());
obj.wait();
System.out.println("wait()方法结束.. " +
Thread.currentThread().getName());
return;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public void notifyMethod() {
synchronized (obj) {
try {
System.out.println("notify()方法开始.. " + Thread.currentThread().getName());
obj.notify();
System.out.println("notify()方法结束.. " + Thread.currentThread().getName());
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void run() {
if (flag) {
this.waitMethod();
} else {
this.notifyMethod();
}
}
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
TestNotify waitThread = new TestNotify(true, object);
TestNotify notifyThread = new TestNotify(false, object);
Thread thread1 = new Thread(waitThread, "wait线程");
Thread thread2 = new Thread(notifyThread, "notify线程");
thread1.start();
Thread.sleep(1000);
thread2.start();
System.out.println("main方法结束!!");
}
}
2.2.3.4、notifyAll方法
此方法会唤醒所有的在等待中的线程,使其正常执行
package thread;
class TestNotifyAll implements Runnable {
private boolean flag;
private Object obj;
public TestNotifyAll(boolean flag, Object obj) {
super();
this.flag = flag;
this.obj = obj;
}
public void waitMethod() {
synchronized (obj) {
try {
while (true) {
System.out.println("wait()方法开始.. " +
Thread.currentThread().getName());
obj.wait();
System.out.println("wait()方法结束.. " +
Thread.currentThread().getName());
return;
}
} catch (Exception e) {e.printStackTrace();
}
}
}
public void notifyMethod() {
synchronized (obj) {
try {
System.out.println("notifyAll()方法开始.. " +
Thread.currentThread().getName());
obj.notifyAll();
System.out.println("notifyAll()方法结束.. " +
Thread.currentThread().getName());
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void run() {
if (flag) {
this.waitMethod();
} else {
this.notifyMethod();
}
}
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
TestNotifyAll waitThread1 = new TestNotifyAll(true, object);
TestNotifyAll waitThread2 = new TestNotifyAll(true, object);
TestNotifyAll waitThread3 = new TestNotifyAll(true, object);
TestNotifyAll notifyThread = new TestNotifyAll(false, object);
Thread thread1 = new Thread(waitThread1, "wait线程A");
Thread thread2 = new Thread(waitThread2, "wait线程B");
Thread thread3 = new Thread(waitThread3, "wait线程C");
Thread thread4 = new Thread(notifyThread, "notify线程");
thread1.start();
thread2.start();
thread3.start();
Thread.sleep(1000);
thread4.start();
System.out.println("main方法结束!!");
}
}
2.3、线程状态
线程一共有6种状态。
3、线程间同步
线程间同步的含义是线程间对临界资源操作的合理性;那生产者和消费者模型举个例子,生产者生产三个资源,消费者就能消费三个资源,当资源不足时消费者要通知生产者生产资源消费者等待,生产完资源后生产者要通知消费者消费,生产者等待。
3.1、Semaphore信号量
信号量:表示可用资源的数量,本质上就是一个计数器,比如停车场有100个车位,那么就只有100个资源,停进去一辆资源-1,出来一辆资源+1,当资源为0的时候,想进停车场就得等待
方法 | 说明 |
semaphore.acquire() | 对资源-1 |
semaphore.release() | 对资源+1 |
package thread;
import java.util.concurrent.Semaphore;
public class TestSemaphore {
//信号量
public static Semaphore semaphore = new Semaphore(3);
public void testSemaphoreFunc()
{
System.out.println("我的名字是:"+Thread.currentThread().getName());
try {
semaphore.acquire();
System.out.println("成功获取到一个资源,我的名字是:"+ Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("资源使用完毕,我的名字是:"+ Thread.currentThread().getName());
semaphore.release();
}
public static void main(String[] args) throws InterruptedException {
TestSemaphore testSemaphore = new TestSemaphore();
for (int i = 0; i < 10; ++i) {
Thread thread = new Thread(() -> testSemaphore.testSemaphoreFunc());
thread.start();
thread.join();
}
//定义一个任务
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("我的ID是:"+Thread.currentThread().getName());
try {
semaphore.acquire();
System.out.println("成功获取到一个资源,我的ID是:"+ Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("资源使用完毕,我的ID是:"+ Thread.currentThread().getName());
semaphore.release();
}
};
for (int i = 0; i < 10; ++i) {
Thread thread = new Thread(runnable);
thread.start();
thread.join();
}
}
}
3.2、volatile关键字
修饰的共享变量,可以保证可见性。
可见性的含义:为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线程之间不能及时看到改变。
将共享变量用volatile修饰,线程对变量做了改变就能及时刷新进主内存中,保证变量可见性。
3.3、条件变量
java中没有与C++中条件变量一样的工具,但是 Java 提供了等待/通知机制来实现类似的功能。这个机制是通过线程对象的 wait()
、notify()
和 notifyAll()
方法来实现的。
4、线程间互斥
参考另一篇文章,待续。