1.1 进程和线程
进程Process: 每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1—n个线程。可以把进程简单理解为操作系统中运行的一个程序
线程Thread:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。
多进程是指操作系统能同时运行多个任务(程序)。
多线程是指在同一程序中有多个顺序流在执行。进程是线程的容器,一个进程里可以运行1-N个线程,操作系统以进程为单位分配资源
线程是进程的一个执行分支
下面是两条公路,相当于两个进程。左边是单线程的,右边是多线程的。
1.2 实现线程的方式
在JAVA中,创建线程就是创建Thread类(子类)的对象(实例)
java.lang.Thread
- public class Thread extends Object implements Runnable
构造方法摘要
| Constructor and Description | | —- | | Thread()
分配一个新的 Thread对象。 | | Thread(Runnable target)
分配一个新的 Thread对象。 |
1.2.1 继承Thread类
class ThreadTest extends Thread{
public void run(){
System.out.println("这个线程开始运行");
}
}
public class Test {
public static void main(String[] args) {
ThreadTest tt = new ThreadTest ();
tt.start(); //启动一个线程
}
}
注意:主函数使用start方法启动新线程,而不是调用run方法,区别如下:
1.2.2 实现Runnable接口(优先使用)
class ThreadTest implements Runnable{
public void run(){
System.out.println("这个线程开始运行");
}
}
public class Test {
public static void main(String[] args) {
ThreadTest tt = new ThreadTest ();
Thread t = new Thread(tt);
t.start(); //通过一个线程对象启动它
}
}
这两种方式的区别:
1、继承Thread类:编写简单,可直接操作线程。适用于单继承
2、实现Runnable接口:避免单继承局限性,便于共享资源。
推荐使用实现Runnable接口方式创建线程
思考:为什么不直接调用run()方法?
不管调用什么方法,如何调用,都是只有主线程一条执行路径。
1.2.3 实现Callable接口(了解)
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
//创建Callable接口实现类,并实现call()方法,此方法为线程执行体,且有返回值
class CallableThreadTest implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println("执行线程");
return 1;
}
}
public class Test {
public static void main(String[] args) {
//使用FutureTask类(实现了Future和Runnable接口)来包装Callable对象
FutureTask<Integer> ft = new FutureTask<>(new CallableThreadTest());
//使用FutureTask对象作为Thread对象的target创建并启动新线程
new Thread(ft).start();
//调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
try {
System.out.println("子线程的返回值:" + ft.get());
} catch (Exception e) {
e.printStackTrace();
}
}
}
Runnable和Callable的区别:
- Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
- Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
- call方法可以抛出异常,run方法不可以。
- 运行Callable任务可以拿到一个Future对象。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
1.3 线程生命周期
线程对象的生命周期,也就是线程在五种状态之间的转换。
- [NEW]新建状态
尚未启动的线程处于此状态。新创建了一个线程对象,但未调用start方法 - [RUNNABLE]就绪状态,即可运行状态,复合状态
包含ready和running两个状态
ready:表示该线程处于可被调度调度的状态
运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
Thread.yield()方法可以将线程由Running状态变为ready 状态 - [BLOCKED]阻塞状态
被阻塞等待监视器锁定的线程处于此状态。
线程发起I/O操作或者申请由其他线程独占的资源,不占用CPU
当阻塞I/O执行完毕可或者获得了申请的资源,状态就变为RUNNABLE
阻塞I/O是指,进程会一直阻塞,直到数据拷贝完成;阻塞I/O模式时最普遍使用的I/O模式,如输入、输出、接受连接等
[WAITING]
正在等待另一个线程执行特定动作的线程处于此状态。
线程执行了object.wait() 或thread.join()方法就会把线程转换为等待状态,执行object.notify()方法或者加入的线程执行完毕,线程将重回RUNNABLE状态(傻傻的等)
[TIMED_WAITING]
正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。
与WAITING的区别是不会无限期的等待,而是等待指定时间后不管期望的操作是否执行完毕,线程都将重回RUNNABLE状态(只等等指定长度的时间)
[TERMINATED]终止状态
已退出的线程处于此状态。
1.4 线程运行的不确定性
注意:并不是调用start()方法后,线程就开始运行。而是线程进入就绪状态,等待调度来运行它。所以一个线程的运行是不确定的。
实例:
class ThreadTest extends Thread{
public void run(){
for(int i=0;i<100;i++){
System.out.println(this.getName() + ":" + i);
}
}
}
public static void main(String[] args) {
ThreadTest tt1 = new ThreadTest ();
ThreadTest tt2 = new ThreadTest ();
tt1.start(); //运行结果并不确定!
tt2.start(); //运行结果并不确定!
System.out.println(tt1.isAlive()); //当执行输出语句时,线程是否存活也不确定
System.out.println(tt2.isAlive()); //当执行输出语句时,线程是否存活也不确定
}
1.5 线程调度常用方法
方法 | 功能 |
---|---|
getName() setName() |
设置或获取当前线程名称 |
isAlive() | 判断线程是否存活(就绪、运行、阻塞是存活状态) |
getPriority() | 获取线程优先级 |
setPriority() | 设置线程优先级 |
Thread.sleep() | 强迫线程睡眠(单位:毫秒)(不释放同步锁) |
join() | 等待某线程结束,再恢复当前线程的运行 |
yield() | 让出CPU资源,当前线程进入就绪状态 |
wait() | 当前线程进入等待状态,即进入等待池。(释放同步锁,由notify()唤醒) |
notify() notifyAll() |
唤醒等待池中的某个或全部等待线程 |
1.5.1 线程名字
设置线程setName()
获取线程名字getName()
RunThread01 r1=new RunThread01();
Thread t1=new Thread(r1);
t1.setName("线程01");
t1.start();
public class RunThread01 implements Runnable {
@Override
public void run() {
for(int i=0;i<=100000;i++){
System.out.println(Thread.currentThread().getName());
}
}
}
1.5.2 判断线程是否存活
ThreadTest t1 = new ThreadTest();
ThreadTest t2 = new ThreadTest();
t1.start();
t2.start();
try {
Thread.sleep(1000); //让主线程睡眠1秒,t1线程肯定死亡。
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t1.isAlive());
1.5.3 线程的优先级
Java线程有优先级,优先级高的线程会获得
较多的CPU运行机会。
Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。
Java线程的优先级用整数表示,取值范围是1~10; 10最高级。
每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY(5)。
ThreadTest t1 = new ThreadTest();
t1.setPriority(1); //设置成最低级
ThreadTest t2 = new ThreadTest();
t2.setPriority(10); //设置成最高级
t1.start();
t2.start();
1.5.4 线程睡眠
Thread.sleep() 强迫线程睡眠(单位:毫秒)
class ThreadTest extends Thread{
public void run(){
for(int i=0;i<10;i++){
System.out.println(i);
try {
Thread.sleep(1000); //每隔一秒后输出
} catch (InterruptedException e) { //线程睡眠期间如果被打断,将抛出异常。
e.printStackTrace();
}
}
}
}
1.5.5 合并线程
join()的功能是:等待某线程结束,再恢复当前线程的运行
或者说:join()把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。
class ThreadTest extends Thread{
public void run(){
for(int i=0;i<10;i++){
System.out.println(this.getName() + ":" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//相当于将tt线程和main线程合并成一个线程。
//那么main线程会等待tt线程结束后再运行。
public static void main(String[] args) {
ThreadTest tt = new ThreadTest ();
tt.start();
try {
tt.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=0;i<10;i++){
System.out.println("main:" + i);
}
}
1.5.6 让出CPU资源
yield() 让出CPU资源,当前线程进入就绪状态
class ThreadTest extends Thread{
public void run(){
for(int i=0;i<100;i++){
System.out.println(this.getName() + ":" + i);
if(i%10==0){
Thread.yield();
}
}
}
}
public static void main(String[] args) {
ThreadTest tt1 = new ThreadTest ();
ThreadTest tt2 = new ThreadTest ();
tt1.start();
tt2.start();
}
运行结果:
运行到10时,线程让出CPU资源,进入就绪状态。
注意: yield()是让出资源,但并不放弃。它会进入就绪状态,也就是说:它还会与其他线程
一起抢占资源,所以yield()的线程,仍然有可能再次抢占资源。
在加上线程运行的不确定性,所以会导致上面的结论并不是绝对的,只是出现的
概率要高一些。
1.5.7 线程等待与唤醒(涉及线程同步,在后续生产者消费者部分进行介绍)
wait():当前线程进入等待状态,即进入等待池。(释放同步锁,由notify()唤醒);
notify()/notifyAll():唤醒等待池中的某个或全部等待线程。
通俗的说:
wait()意思是说,我等会儿再用这把锁,CPU也让给你们,我先休息一会儿!
notify()意思是说,我用完了,你们谁用?
也就是说,wait()会让出对象锁,同时,当前线程休眠,等待被唤醒,如果不被唤醒,就一直等在那儿。
notify()并不会让当前线程休眠,但会唤醒休眠的线程。
注意:
wait会让出CPU而notify不会,notify重在于通知使用object的对象“我用完了!”;
wait重在通知其它同用一个object的线程“我暂时不用了”并且让出CPUT。
1.6 线程的中断
线程的中断
1、自动中断:一个线程完成执行后(即run方法执行完毕),不能再次运行 。
2、手动中断:
stop( ) —— 已过时,基本不用。(不安全,就像是突然停电)
interrupt( ) ——此方法只是改变中断状态,不会中断一个正在运行的线程。
比如:如果当前线程是阻塞状态,那么就结束阻塞状态
3、可通过使用一个标志指示 run 方法退出,从而终止线程(推荐使用)
通常,线程中断的使用场景有以下几个:
- 点击某个桌面应用中的取消按钮时;
- 某个操作超过了一定的执行时间限制需要中止时;
- 多个线程做相同的事情,只要一个线程成功其它线程都可以取消时;
- 一组线程中的一个或多个出现错误导致整组都无法继续时;
- 当一个应用或服务需要停止时。
interrupt( )方法说明:
interrupt()方法只是改变中断状态,不会中断一个正在运行的线程。这一方法实际完成的是,给受阻塞的线程发出一个中断信号,这样受阻线程检查到中断标识,就得以退出阻塞的状态。
更确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,此时调用该线程的interrupt()方法,那么该线程将抛出一个 InterruptedException中断异常(该线程必须事先预备好处理此异常),从而提早地终结被阻塞状态。如果线程没有被阻塞,这时调用 interrupt()将不起作用,直到执行到wait(),sleep(),join()时,才马上会抛出 InterruptedException。
class ThreadTest extends Thread{
@Override
public void run() {
for(int i=0;i<100;i++) {
System.out.println(this.getName()+":"+i);
if(i>10) {
this.interrupt();
}
try {
this.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Test05 {
public static void main(String[] args) {
ThreadTest tt1 = new ThreadTest();
tt1.setName("线1");
tt1.start();
ThreadTest tt2 = new ThreadTest();
tt2.setName("线2");
tt2.start();
}
}
1.7 线程同步问题
当多个线程同时操作同一个数据时,就会产生线程同步问题。
为了确保在任何时间点一个共享的资源只被一个线程使用,使用了“同步”
当一个线程运行到需要同步的语句后,CPU不去执行其他线程中的、可能影响当前线程中的下一句代码的执行结果的代码块,必须等到下一句执行完后才能去执行其他线程中的相关代码块,这就是线程同步。
下面这个例子中,多个线程共同操作同一个账户里的余额,就有可能出现线程同步错误。
//模拟一个账户,其中有余额1000元。取钱时如果不足1000元就不能取。
class Account{
private int balance = 1000;
public void qu(){
if(balance>=1000){
balance -= 1000;
System.out.println("取了1000元");
}
}
}
class ThreadTest extends Thread{
private Account account;
public ThreadTest(Account account){
this.account = account;
}
public void run(){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.qu(); //在线程类中调用取钱的方法
}
}
//模拟多个人同时操作同一个账户。结果有可能出现这样一种情况:
//账户中共有余额1000元,但却被取走了2000元。
public static void main(String[] args) {
Account account = new Account();
ThreadTest tt1 = new ThreadTest(account); //给每个线程传入的是同一个账户
ThreadTest tt2 = new ThreadTest(account); //给每个线程传入的是同一个账户
tt1.start();
tt2.start();
}
1.8线程同步解决方案一synchronized
解决办法:加上synchronized关键词,使取钱的方法成为一个同步方法。
一旦一个包含锁定方法(用synchronized修饰)的线程被CPU调用,其他线程就无法调用相同对象的锁定方法。当一个线程在一个锁定方法内部,所有试图调用该方法的同实例的其他线程必须等待
synchronized:同步锁(互斥锁):
在java语言中,引入了同步锁的概念,每个对象都有一个与之关联的内部锁(排他锁),用以保证共享数据的安全性问题。
关键词synchronized用来给某个方法或某段代码加上一个同步锁。
当调用者调用此方法时,必须获得这把锁才可以调用。
当某个调用者获得这把锁之后,其他调用者就无法获得了。
当调用结束后,调用者释放这把锁,此时其他调用者才可以获得。
这个机制保障了某个同步方法同时只能有一个调用者。
1、锁定方法
class Account{
private int balance = 1000;
public synchronized void qu(){ //同步方法
if(balance>=1000){
balance -= 1000;
System.out.println("取了1000元");
}
}
}
也可以这样写:
2、锁定代码块
class Account{
private int balance = 1000;
public void qu(){
synchronized(this){ //同步块
if(balance>=1000){
balance -= 1000;
System.out.println("取了1000元");
}
}
}
}
1.9 死锁问题
同步锁具有互斥作用,即排他性。但是这又造成了另外一个问题:死锁。
为了完成一个功能,需要调用两个资源。但是,当两个线程同时调用这两
个资源时,就会出现这样的现象:两个线程都不放弃抢到的一个资源,而另一个资源却永远也抢不到。
实例:
public class TestLock {
public static String objA = "strA";
public static String objB = "strB";
public static void main(String[] args) {
Lock1 l1=new Lock1();
Thread t1=new Thread(l1);
Lock2 l2=new Lock2();
Thread t2=new Thread(l2);
t1.start();
t2.start();
}
}
public class Lock1 implements Runnable{
@Override
public void run() {
try{
System.out.println("Lock1 running");
while(true){
synchronized(TestLock.objA){
System.out.println("Lock1 lock strA");
Thread.sleep(5000);//获取strA后先等一会儿,让Lock2有足够的时间锁住strB
synchronized(TestLock.objB){
System.out.println("Lock1 lock strB");
}
}
}
}catch(Exception e){
e.printStackTrace();
}
}
}
package com.neuedu.thread.lock;
public class Lock2 implements Runnable{
@Override
public void run() {
try{
System.out.println("Lock2 running");
while(true){
synchronized(TestLock.objB){
System.out.println("Lock1 lock strB");
Thread.sleep(5000);//获取strA后先等一会儿,让Lock2有足够的时间锁住strB
synchronized(TestLock.objA){
System.out.println("Lock1 lock strA");
}
}
}
}catch(Exception e){
e.printStackTrace();
}
}
}
总结:
一、产生死锁的必要条件:
虽然线程在运行过程中可能会发生死锁,但产生死锁是必须具备一定条件的。产生死锁必须同时具备下面四个必要条件,只要其中任意一个条件不成立,死锁就不会产生:
(1)互斥条件:线程对所分配到的资源进行排他性使用,即在一段时间内,某资源只能被一个线程占用。如果此时还有其他进程请求该资源,则请求进程只能等待,直至占有该资源的线程释放该资源。
(2)请求和保持条件:线程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他线程占有,此时请求线程被阻塞,但对自己以获得的资源保持不放。
(3)不可抢占条件:线程已获得的资源在未使用完之前不能被抢占,只能在进程使用完时由自己释放。
(4)循环等待条件。在发生死锁时,必然存在一个线程—资源的循环链,即线程集合{P0,P1,P2,P3,…,Pn}中的P0正在等待P1占用的资源,P1正在等待P2占用的资源,… … ,Pn正在等待已被P0占用的资源。
二、处理死锁的方法
(1)预防死锁。该方法是通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或几个来预防产生死锁。
(2)避免死锁。在资源的动态分配过程中,用某种方法防止系统进入不安全状态,从而可以避免产生死锁。
(3)检测死锁。通过检测机构及时地检测出死锁的发生,然后采取适当的措施,把进程从思索中解脱出来。
(4)解除死锁。当检测到系统中已发生死锁时,就采取相应的措施,将进程从死锁状态中解脱出来。常用方法是—-撤销一些进程,回收他们的资源,将他们分配给已处于阻塞状态的进程,使其能继续运行。
1.10 线程同步的第二种解决方案 wait() notify()
例:生产者消费者问题
生产者-消费者(producer-consumer)问题,也称作有界缓冲区(bounded-buffer)问题,两个线程共享一个公共的固定大小的缓冲区。其中一个是生产者,用于将消息放入缓冲区;另外一个是消费者,用于从缓冲区中取出消息。问题出现在当缓冲区已经满了,而此时生产者还想向其中放入一个新的数据项的情形,其解决方法是让生产者此时进行休眠,等待消费者从缓冲区中取走了一个或者多个数据后再去唤醒它。同样地,当缓冲区已经空了,而消费者还想去取消息,此时也可以让消费者进行休眠,等待生产者放入一个或者多个数据时再唤醒它。
package test06;
//仓库(有界缓冲区)
class Storage{
private int count = 0;
public synchronized void set(){
if(count>=5){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count++;
System.out.println("生产了一个,仓库中有:" + count);
this.notify();
}
public synchronized void get(){
if(count<=0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count--;
System.out.println("消费了一个,仓库中有:" + count);
this.notify();
}
}
//生产者
class Producer extends Thread{
private Storage storage;
public Producer(Storage storage){
this.storage = storage;
}
public void run(){
for(int i=0;i<50;i++){
this.storage.set();
}
}
}
//消费者
class Customer extends Thread{
private Storage storage;
public Customer(Storage storage){
this.storage = storage;
}
public void run(){
for(int i=0;i<50;i++){
this.storage.get();
}
}
}
public class Test2 {
public static void main(String[] args) {
Storage storage = new Storage();
Producer producer = new Producer(storage);
Customer customer = new Customer(storage);
customer.start();
producer.start();
}
}
总结线程同步的常用方法有以下两种:
1、synchronized
2、wait与notify
学生扩展学习:
线程同步还有哪些方法?(volatile lock)
1.10 附录:守护线程
Java的线程分为两种:User Thread(用户线程)、DaemonThread(守护线程)。
用个比较通俗的比喻,任何一个守护线程都是整个JVM中所有非守护线程的保姆。
只要当前JVM实例中尚存任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束是,守护线程随着JVM一同结束工作,Daemon作用是为其他线程提供便利服务,守护线程最典型的应用就是GC(垃圾回收器),他就是一个很称职的守护者。
thread.setDaemon(true) 设置线程为守护线程,该设置必须在thread.start()之前设置
thread.isDaemon() 可以判断当前线程是否为守护线程
课后作业
1、利用Thread实现,要求多线程求解某范围素数每个线程负责1000范围:线程1找1-1000;线程 2 找 1001-2000;线程 3 找2001-3000。编程程序将每个线程找到的素数及时打印。 [必做题]
2、利用Runnable实现,要求多线程求解某范围素数每个线程负责1000范围:线程1找1-1000;线程 2 找 1001-2000;线程 3 找2001-3000。编程程序将每个线程找到的素数及时打印。 [必做题]
3、编写一个Java程序(包括一个主程序类,一个线程类。在主程序类中创建2个线程,将其中一个线程的优先级设为10,另一个线程的优先级设为6。让优先级为10的线程打印200次“线程1正在运行”,优先级为6的线程打印200次“线程2正在运行”。 [选做题]
4、编写一个计时器,每隔一秒钟,在控制台打印出最新时间。 [必做题]