多线程基础篇
1.进程与线程
Java的第一大特色:多线程的编程支持
1.概念
进程:操作系统中一个程序的执行周期称为一个进程。
在DOS系统的时代,由于其本身就是一个单进程的操作系统,所以在同一时间段上只能够有一个程序执行。
后来发展到winodws系统后,我们发现多个程序可以同时执行,所以windows是一个多进程的操作系统。
线程:进程中的独立任务,OS中任务调度的最小单元,依赖进程存在。
与进程相比较,线程更"轻量级",创建、撤销一个线程比启动一个新进程开销要小的多。没有进程就没有线程,进程一旦终止,其内的线程也将不复存在。
2.多进程与多线程区别
本质区别在于,每个进程拥有自己的一整套变量,而线程则共享数据。共享变量使得线程之间的通信比进程之间通信更有效、更方便;
3.多线程的应用
在实际应用中,多线程非常有用。例如,一个浏览器应用可以同时下载多个图片、音乐;一个Web服务器需要同时处理多个并发的请求。这些都是多线程的应用。
4.高并发
高并发:访问的线程量非常非常高。
高并发带来的问题:服务器内存不够用,无法处理新的请求。
2.多线程的实现
1.通过实现继承Thread类来实现多线程
线程的启动是调用start方法
如果调用run方法,跟普通对象调用方法没有区别
- 线程方法(start)只能调用一次,否则抛出IllegalThreadStateException异常
2.通过实现Runnable接口来实现多线程
看看Runnable的源码:
- 可见这是一个函数式接口,内只含一个抽象方法,可以进行函数式编程
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
3.Callable接口实现多线程(有返回值)
注意:这是一个泛型接口,定义的泛型即为call方法的返回类型;
从JDK1.5开始追加了新的开发包:java.uti.concurrent。这个开发包主要是进行高并发编程使用的,包含很多在高并发操作中会使用的类。在这个包里定义有一个新的接口Callable;
使用方法:定义子类实现Callable接口,通过FutureTask和Thread(Runable target)构造实例化Thread对象
Runnable中的run()方法没有返回值,它的设计也遵循了主方法的设计原则:线程开始了就别回头。但是很多时候需要一些返回值,例如某些线程执行完成后可能带来一些返回结果,这种情况下就只能利用Callable来实现多线程。
实例:
//泛型接口,在定义时需要指定类型,否则默认为Object;
class MyThreadC implements Callable<String> {
private int ticket = 10;
@Override
public String call() throws Exception {
while(ticket > 0) {
System.out.println(Thread.currentThread().getName()+"进行了售票,"+"票还剩余:"+ --ticket);
}
return "票已售完!!!";
}
}
public class TestThread01 {
public static void main(String[] args) {
MyThreadC myThreadC = new MyThreadC();
FutureTask<String> futureTask = new FutureTask<>(myThreadC);
new Thread(futureTask).start();
new Thread(futureTask).start();
try {
System.out.println(futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
输出:
Thread-0进行了售票,票还剩余:9
Thread-0进行了售票,票还剩余:8
Thread-0进行了售票,票还剩余:7
Thread-0进行了售票,票还剩余:6
Thread-0进行了售票,票还剩余:5
Thread-0进行了售票,票还剩余:4
Thread-0进行了售票,票还剩余:3
Thread-0进行了售票,票还剩余:2
Thread-0进行了售票,票还剩余:1
Thread-0进行了售票,票还剩余:0
票已售完!!!
这里大家可能会有疑问,不是同时两个开始售票的吗,为甚麽只有Thread-0在售票(默认命名方式),这里在后面就会讲到,在这里我们只需要关注Callable的用法;
4.利用线程池创建多线程(⭐优先考虑)
这里我们留到下一篇博客专门讲解多线程?
附加5. 继承Thread和实现Runnable的区别
- 前者单继承,不能再继承其他类; 后者可实现多继承
- 前者线程的创建调度跟业务耦合; 后者业务独立,可以复用(共享)
对于第二个,举例下面代码:
class MyThreadTick extends Thread {
@Override
public void run() {
int tick = 10;
while(tick > 0) {
System.out.println("剩余票数:"+(tick--));
}
}
}
class MyRunnableTick implements Runnable {
private int tick = 10;
@Override
public void run() {
while(tick > 0) {
System.out.println("剩余票数:"+tick--);
}
}
}
public class TestMyRunnableTick {
public static void main(String[] args) {
//下面这种方式没有对同一对象进行处理
// new MyThreadTick().start();
// new MyThreadTick().start();
// new MyThreadTick().start();
Runnable myRunnableTick = new MyRunnableTick();
new Thread(myRunnableTick).start();
new Thread(myRunnableTick).start();
new Thread(myRunnableTick).start();
}
}
输出:
剩余票数:10
剩余票数:8
剩余票数:7
剩余票数:6
剩余票数:5
剩余票数:4
剩余票数:3
剩余票数:2
剩余票数:1
剩余票数:9
附加6. Runnable的代理模式 ⭐
前面已经知道,Runnable是一个@FunctionalInterface声名的函数式接口,再来看看Thread的几种构造方法:
//第一种
public Thread(Runnable target, String name) {
init(null, target, name, 0);
}
//第二种
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
我们需要知道,要构建一个新的线程,始终要依靠Threa类来完成,所以单单是Runnable类是不行的;
所以我们只需要提供Runnable对象,就可以构造一个新的线程 ,这里其实Thread类就是一个代理类,而我们的Runnable是一个真实业务类,所以这里实现了简单的代理模式 ,以后在遇到举例代理模式的例子时,这无疑不是一个很好的例子!!
3.多线程常用方法
1.获取当前进程名称
在Thread类中提供有这样一个构造方法:
public Thread(Runnable target, String name) {
init(null, target, name, 0);
}
即我们在实例化Thread对象时可以直接为当前线程命名;
public class TestThread04 {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
System.out.println("Thread name :"+thread.currentThread().getName());
new Thread(()-> {
System.out.println("Thread name :"+thread.currentThread().getName());
}
,"线程一").start();
new Thread(()-> {
System.out.println("Thread name :"+thread.currentThread().getName());
}
,"线程二").start();
}
}
线程的方法:
-
通过Thread.currentThread()获取执行当前代码的线程
-
Java的main方法,程序入口,他也在线程中执行
-
线程是Java应用程序执行的最小单元
-
创建线程时,如果不指定线程名, 则默认为:Thread-index(0,1,2,3,4,5…)
创建线程时通过构造方法,或者setName
public class TestThread04 {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
//可见main函数也是一个线程
System.out.println("Thread name :"+thread.currentThread().getName());
new Thread(()-> {
System.out.println("Thread name :"+thread.currentThread().getName());
}
,"线程一").start();
new Thread(()-> {
System.out.println("Thread name :"+thread.currentThread().getName());
}
,"线程二").start();
}
}
输出:
Thread name :main
Thread name :线程一
Thread name :线程二
2.sleep方法(不会释放锁)
线程休眠:指的是让线程暂缓执行一下,等到了预计时间之后再恢复执行。
线程休眠会交出CPU,让CPU去执行其他的任务。但是有一点要非常注意,sleep方法不会释放锁(资源),也就是说如果当前线程持有对某个对象的锁(资源),则即使调用sleep方法,其他线程也无法访问这个对象
class MyRunnableTestSleep implements Runnable {
@Override
public void run() {
for(int i = 0;i<10;i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前线程:"+Thread.currentThread().getName()+" i="+i);
}
}
}
public class TestThread05 {
public static void main(String[] args) {
MyRunnableTestSleep myRunnableTestSleep = new MyRunnableTestSleep();
new Thread(myRunnableTestSleep).start();
new Thread(myRunnableTestSleep).start();
}
}
输出:
当前线程:Thread-1 i=0
当前线程:Thread-0 i=0
当前线程:Thread-0 i=1
当前线程:Thread-1 i=1
当前线程:Thread-1 i=2
当前线程:Thread-0 i=2
当前线程:Thread-1 i=3
当前线程:Thread-0 i=3
当前线程:Thread-0 i=4
当前线程:Thread-1 i=4
当前线程:Thread-1 i=5
当前线程:Thread-0 i=5
通过代码观察会错误的认为这三个线程是同时休眠的,但是千万要记住,所有的代码是依次进入到run()方法中的。真正进入到方法的对象可能是多个,也可能是一个。进入代码的顺序可能有差异,但是总体的执行是并发执行
3.join方法(会释放锁)
等待该线程终止。意思就是如果在主线程中调用该方法时就会让主线程休眠,让调用该方法的线程run方法先执行
完毕之后在开始执行主线程。
例码:
class MyRunnableT implements Runnable {
@Override
public void run() {
int i = 0;
while(i<10) {
System.out.print(i++ +" ");
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("\nfun方法结束!");
}
}
public class TestThread01 {
public static void main(String[] args) {
MyRunnableT myRunnableT = new MyRunnableT();
Thread thread = new Thread(myRunnableT);
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主方法结束!!");
}
}
输出:
0 1 2 3 4 5 6 7 8 9
fun方法结束!
主方法结束!!
假如没有调用join方法,则将会输出:
主方法结束!!
0 1 2 3 4 5 6 7 8 9
fun方法结束!
4.yield()方法(线程让步)(不释放锁)
yield() : 让步,交出CPU时间不确定,由系统调度决定,只会让拥有相同优先级的线程有获取CPU的机会,不释放锁;
4.线程停止的3种方法(⭐)
线程的停止是多线程开发当中的一个非常重要的技术;
1. 设置标记位,可以使线程正常退出
class MyRunnableT implements Runnable {
private boolean flag = true;
@Override
public void run() {
int i = 1;
while(flag) {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread()+"第"+i+"次执行");
i++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
public class TestThread01 {
public static void main(String[] args) {
MyRunnableT myRunnableT = new MyRunnableT();
Thread thread = new Thread(myRunnableT,"线程一");
thread.start();
try {
Thread.sleep(4000); //主线程休眠2000;不妨碍其他线程的执行,此时线程一仍然在执行;
} catch (InterruptedException e) {
e.printStackTrace();
}
myRunnableT.setFlag(false);
System.out.println("主方法结束!");
}
}
Thread[线程一,5,main]第1次执行
Thread[线程一,5,main]第2次执行
Thread[线程一,5,main]第3次执行
主方法结束!
Thread[线程一,5,main]第4次执行
2. 使用stop方法强制使线程退出(但是该方法不太安全所以已经被废弃了)
class MyRunnableT implements Runnable {
@Override
public void run() {
int i = 1;
while(true) {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread()+"第"+i+"次执行");
i++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class TestThread01 {
public static void main(String[] args) {
MyRunnableT myRunnableT = new MyRunnableT();
Thread thread = new Thread(myRunnableT,"线程一");
thread.start();
try {
Thread.sleep(4000); //主线程休眠2000;不妨碍其他线程的执行,此时线程一仍然在执行;
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.stop(); //在编译器中,这是过期代码(一条横线穿过)
System.out.println("主方法结束!");
}
}
Thread[线程一,5,main]第1次执行
Thread[线程一,5,main]第2次执行
Thread[线程一,5,main]第3次执行
主方法结束!
3.利用中断来终止
class MyRunnableT implements Runnable {
private boolean flag = true;
@Override
public void run() {
int i = 1;
while(flag) {
try {
Thread.sleep(1000);
boolean bool = Thread.currentThread().isInterrupted();
if(bool) {
System.out.println("非阻塞情况下执行该操作 线程状态:"+bool);
break;
}
System.out.println(Thread.currentThread().getName()+"第"+i+"次执行");
i++;
} catch (InterruptedException e) {
System.out.println("exit!!!");
boolean bool = Thread.currentThread().isInterrupted();
System.out.println(bool);
return;
}
}
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
public class TestThread01 {
public static void main(String[] args) {
MyRunnableT myRunnableT = new MyRunnableT();
Thread thread = new Thread(myRunnableT,"线程A");
thread.start();
try {
Thread.sleep(3000);
thread.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main方法结束!!!");
}
}
线程A第1次执行
线程A第2次执行
main方法结束!!!
exit!!!
false
上面的代码是通过异常退出的,这是因为run方法中有sleep方法的阻塞,从而从产生异常这一方式退出线程;
详细解析
interrupt() 方法只是改变中断状态而已,它不会中断一个正在运行的线程。这一方法实际完成的是,给受阻塞的线程发出一个中断信号,这样受阻线程就得以退出阻塞的状态。
然而interrupte()方法并不会立即执行中断操作;具体而言,这个方法只会给线程设置一个为true的中断标志(中断标志只是一个布尔类型的变量),而设置之后,则根据线程当前的状态进行不同的后续操作。如果,线程的当前状态处于非阻塞状态,那么仅仅是线程的中断标志被修改为true而已;如果线程的当前状态处于阻塞状态,那么在将中断标志设置为true后,还会有如下三种情况之一的操作:
- 如果是wait、sleep以及join三个方法引起的阻塞,那么会将线程的中断标志重新设置为false,并抛出一个
InterruptedException;
- 如果在中断时,线程正处于非阻塞状态,则将中断标志修改为true,而在此基础上,一旦进入阻塞状态,则按照阻塞状态的情况来进行处理;例如,一个线程在运行状态中,其中断标志被设置为true之后,一旦线程调用了wait、jion、sleep方法中的一种,立马抛出一个InterruptedException,且中断标志被程序会自动清除,重新设置为false。
通过上面的分析,我们可以总结:
调用线程类的interrupted方法,其本质只是设置该线程的中断标志,将中断标志设置为true,并根据线程状态决定是否抛出异常。 因此,通过interrupted方法真正实现线程的中断原理是:开发人员根据中断标志的具体值,来决定如何退出线程。
5.守护线程和用户线程(垃圾回收线程是守护,main不是)
守护线程是一种特殊的线程,它属于是一种陪伴线程。简单点说 java 中有两种线程:用户线程和守护线程。可以通过isDaemon()方法 来区别它们:如果返回false,则说明该线程是“用户线程”;否则就是“守护线程”。
典型的守护线程就是垃圾回收线程。 只要当前JVM进程中存在任何一个非守护线程没有结束,守护线程就在工作;只有当最后一个非守护线程结束时,守护线程才会随着JVM一同停止工作。
注意:主线程main是用户线程