多线程执行过程
主线程:
public class TestThread {
public static void main(String[] args) {
MyThread mt = new MyThread("小强");
mt.start();
for (int i = 0; i < 20000 ; i++) {
System.out.println("旺财"+i);
}
}
}
自定义线程
public class MyThread extends Thread{
public MyThread(String name){
super(name);
}
@Override
public void run() {
for (int i = 0; i < 20000; i++) {
String name = Thread.currentThread().getName();
System.out.println(name + i);
}
}
}
程序启动运行 main时候,java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着调用mt的对象的start方法,另外一个新的线程也启动了,这样,整个应用就在多线程下运行。
通过这张图我们可以很清晰的看到多线程的执行流程,那么为什么可以完成并发执行呢?我们再来讲一讲原理。
多线程执行时,到底在内存中是如何运行的呢?以上个程序为例,进行图解说明:
多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。
当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那么进程就结束了。
创建线程的方法一
继承Thread,如上的代码所示
创建线程的方法二
实现Runable接口
public class MyRunable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20000; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
}
}
public class TestThread {
public static void main(String[] args) {
MyRunable myRunable = new MyRunable();
Thread th = new Thread(myRunable, "小强");
th.start();
for (int i = 0; i < 20000 ; i++) {
System.out.println("旺财"+i);
}
}
}
通过实现 Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个执行目标。所有的多线程
代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。
在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread
对象的start()方法来运行多线程代码。
实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现
Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程
编程的基础。
tips:Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。
而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。
创建线程的方法三
实现Callable接口
public class TestNewIo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ThreadDemo td = new ThreadDemo();
//1.执行 Callable 方式,需要 FutureTask 实现类的支持,用于接收运算结果。
FutureTask<Integer> result = new FutureTask<Integer>(td);
new Thread(result).start();
//2.接收线程运算后的结果
Integer sum = result.get(); //FutureTask 可用于 闭锁 类似于CountDownLatch的作用,在所有的线程没有执行完成之后这里是不会执行的
System.out.println(sum);
System.out.println("------------------------------------");
}
}
class ThreadDemo implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 10; i++) {
sum += i;
System.out.println(currentThread().getName()+i);
}
return sum;
}
}
创建线程的方法四
参考:线程池
Runnable和Callable的区别
相同点
1、两者都是接口;
2、两者都可用来编写多线程程序;
3、两者都需要调用Thread.start()启动线程;
不同点
1、两者最大的不同点是:实现Callable接口的任务线程能返回执行结果;而实现Runnable接口的任务线程不能返回结果;
2、Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛;
注意点
Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取‘将来’结果;当不调用此方法时,主线程不会阻塞!
Thread 和Runnable的区别
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
实现Runnable接口比继承Thread类所具有的优势:
-
适合多个相同的程序代码的线程去共享同一个资源。
-
可以避免java中的单继承的局限性。
-
增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
-
线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。
扩充:在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用
java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进
程。
使用匿名内部类的方式实现Runable接口
public class TestThread {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
};
new Thread(runnable,"小强").start();
System.out.println("主线程");
}
}
并发与并行
并发:单线程交替执行多任务
并行:多线程同时执行多任务
同步与异步
同步,就是调用某个东西是,调用方得等待这个调用返回结果才能继续往后执行。
异步,和同步相反 调用方不会理解得到结果,而是在调用发出后调用者可用继续执行后续操作,被调用者通过状体来通知调用者,或者通过回掉函数来处理这个调用
阻塞与非阻塞
阻塞:阻塞调用是指调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回。
非阻塞:非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
线程安全问题
多个线程并发访问同一个资源就容易出现线程安全问题
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写
操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,
否则的话就可能影响线程安全。
线程同步
为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。
实现同步的三种方法:
- 同步代码块
- 同步方法
- 锁机制
同步代码块
synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
格式:
synchronized(同步锁){
需要同步操作的代码
}
同步锁:
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.
- 锁对象 可以是任意类型。
- 多个线程对象 要使用同一把锁。
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着
(BLOCKED)。
同步方法
使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
public synchronized void method(){
可能会产生线程安全问题的代码
}
同步锁
java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
Lock锁也称同步锁,加锁与释放锁方法化了,如下:
public void lock() :加同步锁。
public void unlock() :释放同步锁。
public static void main(String[] args) {
Lock lock = new ReentrantLock();
lock.lock();
try {
//业务代码
}finally {
lock.unlock();
}
System.out.println("主线程");
}
这里用到的Lock是一个接口,而ReentrantLock是它的实现,叫可重入锁,synchronized也是java的可重入锁
可重入锁:什么是 “可重入”,可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。
// 演示可重入锁是什么意思,可重入,就是可以重复获取相同的锁,synchronized和ReentrantLock都是可重入的
// 可重入降低了编程复杂性
public class WhatReentrant {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (this) {
System.out.println("第1次获取锁,这个锁是:" + this);
int index = 1;
while (true) {
synchronized (this) {
System.out.println("第" + (++index) + "次获取锁,这个锁是:" + this);
}
if (index == 10) {
break;
}
}
}
}
}).start();
}
}
线程方法:
sleep() :
让线程进入睡眠状态,睡眠到期后进入可运行状态,而不是进入运行状态
wait() :
一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。
public class TestThread {
static Object object = new Object();
public static void main(String[] args) {
new Thread(
new Runnable() {
@Override
public void run() {
while (true){
synchronized (object){
try {
System.out.println(Thread.currentThread().getName()+" === 获取到锁对象,调用wait方法,进入waiting状态,释放锁对象");
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" === 从wait状态醒来,获取到锁对象继续执行");
}
}
}
},"等待线程"
).start();
new Thread(
new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName()+" === 等待3s");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//要唤醒则要先获得对象的锁
synchronized (object){
System.out.println(Thread.currentThread().getName()+" === 获取到锁对象,调用notify方法,释放锁对象");
object.notify();
}
}
},"唤醒线程"
).start();
}
}
通过上述案例我们会发现,一个调用了某个对象的 Object.wait 方法的线程会等待另一个线程调用此对象的Object.notify()方法 或 Object.notifyAll()方法。
其实waiting状态并不是一个线程的操作,它体现的是多个线程间的通信,可以理解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间又存在协作关系。就好比在公司里你和你的同事们,你们可能存在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。
当多个线程协作时,比如A,B线程,如果A线程在Runnable(可运行)状态中调用了wait()方法那么A线程就进入了Waiting(无限等待)状态,同时失去了同步锁。假如这个时候B线程获取到了同步锁,在运行状态中调用了notify()方法,那么就会将无限等待的A线程唤醒。注意是唤醒,如果获取到锁对象,那么A线程唤醒后就进入Runnable(可运行)状态;如果没有获取锁对象,那么就进入到Blocked(锁阻塞状态)。
object.wait()是设置当前线程在该对象上等待,直到有线程调用obj.notify()方法(或notifyAll()方法)。当调用wait()方法后,该线程会进入一个等待队列,等待队列中可能有多个线程,notify()会随机唤醒其中一个线程,而notifyAll()会唤醒所有线程。
wait()和notify()方法必须在sychronized代码块中,调用这些方法时都需要先获得目标对象的一个监视器,然后调用这些方法时会释放监视器
与sleep不同的是,sleep()会一直占有所持有的锁,而wait()会释放锁。
————————————————
线程中断方法
public void interrupt();
public boolean isInterrupted();
public static boolean interrupted();
三个方法很相似,线程中断只是通知目标线程有人希望你退出,而并不是使目标线程退出。
第一个方法是通知目标线程中断,即设置目标线称的中断标志位;
第二个方法判断当前线程是否被中断,如果被中断(即中断标志位被设置),则返回true,否则返回false;
第三个方法判断当前线程的中断状态,并清除该线程的中断标志位(也就意味着,如果连续调用两次该方法,并且中间没有再次设置中断标志位,第二次会返回false,因为中断标志位已经被清除)。
设置优先级方法
public static void main(String[] args) { Thread thread = new Thread( () -> System.out.println("test"), "自定义线程" ); thread.setPriority(Thread.MIN_PRIORITY); thread.setPriority(Thread.MAX_PRIORITY); thread.setPriority(Thread.NORM_PRIORITY); thread.start(); System.out.println("主线程");}
等待线程(join)和谦让(yield)
public final void join() throws InterruptedException;
public static native void yield();
如果一个线程的执行需要另一个线程的参与(比如当前线程执行需要另一个线程执行完毕才能继续执行),这时候可以调用join()方法。t1.join()方法表示等待线程t1执行完毕之后,当前线程再继续执行。当然也可以给join()设置时间参数。
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+"开始执行");
Thread thread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+"正在打印:"+i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"newThread");
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"结束执行");
}
可以看出它是利用wait方法来实现的,上面的例子当main方法主线程调用线程t的时候,main方法获取到了t的对象锁,而t调用自身wait方法进行阻塞,只要当t结束或者到时间后才会退出,接着唤醒主线程继续执行。millis为主线程等待t线程最长执行多久,0为永久直到t线程执行结束。可以主动指定时间
注:join()的本质是让调用线程wait()在当前线程对象实例上,其部分源码如下:
while (isAlive()) { wait(0);}
当线程执行完毕后,它会让被等待的线程在退出前调用notifyAll()通知所有等待的线程继续执行。因此不要在Thread对象实例上使用类似wait()或者notify()等方法。
**yield()方法是使当前线程让出CPU,但该线程会再次抢夺CPU。**就是和同一优先级的线程一起抢夺CPU
守护线程
在Java中有两类线程:用户线程 (User Thread)、守护线程 (Daemon Thread)。 在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。
最典型的守护线程有:JVM来及回收和异常处理
所谓守护 线程,是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。
用户线程和守护线程两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。在守护线程中创建的线程还是守护线程
创建守护线程
public static void main(String[] args) { Thread thread = new Thread( () -> System.out.println("test"), "守护线程" ); thread.setDaemon(true); //必须启动前调用 thread.start(); System.out.println("主线程");}
Lambda表达式:创建接口对象
函数式接口,即只有一个方法的接口
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+": myThread线程执行");
},"myThread").start();
System.out.println(Thread.currentThread().getName()+": main线程执行");
}