文章目录
- 1.并发编程线程基础
- 1.1什么是线程和进程?
- 1.2线程创建与运行
- 1.2.1继承Thread类
- 1.2.2实现Runnable接口
- 1.2.3实现Callable接口(与线程池搭配使用)
- 1.2.4小结
- 1.3线程常用方法
- 1.3.1线程等待与通知
- 1.3.2线程睡眠
- 1.3.3让出CPU执行权
- 1.3.4线程中断
- 1.4理解线程上下文切换
- 1.5线程死锁
- 1.5.1什么是线程死锁?
- 1.5.2如何避免死锁?
1.并发编程线程基础
1.1什么是线程和进程?
- 线程是进程中的一个实体,线程本身是不会独立存在的,线程则是进程的一个执行路径
- 进程是系统进行资源分配的基本单位,线程是CPU分配的基本单位
- 进程例子:我们在电脑上启动的一个个应用,比如我们启动一个浏览器,就会启动了一个浏览器进程
- 线程例子:在 Java 程序中启动的一个 main 函数,即启动了一个JVM进程,而main函数所在的线程就是这个进程中的一个线程,称为主线程
1.2线程创建与运行
- Java中创建线程主要有三种⽅式,分别为继承Thread类、实现Runnable接口、实现Callable接口。
1.2.1继承Thread类
- 继承Thread类,重写run()⽅法,调⽤start()⽅法启动线程
public class ThreadTest {
/**
* 继 承Thread类
**/
public static class MyThread extends Thread {
@Override
public void run () {
System.out.println( "This is child thread" ) ;
}
}
public static void main ( String [] args) {
MyThread thread = new MyThread ();
thread.start();
}
}
1.2.2实现Runnable接口
- 实现 Runnable 接口,重写
run()
方法 - 然后创建 Thread 对象,将 Runnable 对象作为参数传递给 Thread 对象,调用
start()
方法启动线程。
class RunnableTask implements Runnable {
public void run() {
System.out.println("上岸、上岸!");
}
public static void main(String[] args) {
RunnableTask task = new RunnableTask();
Thread thread = new Thread(task);
thread.start();
}
}
1.2.3实现Callable接口(与线程池搭配使用)
- 实现 Callable 接口,重写
call()
方法 - 然后创建 FutureTask 对象,参数为 Callable 对象;紧接着创建 Thread 对象,参数为 FutureTask 对象,调用
start()
方法启动线程。 - 通过 实现Callable接口的对象 的get方法获取返回结果
class CallableTask implements Callable<String> {
public String call() {
return "上岸、上岸了!";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
CallableTask task = new CallableTask();
FutureTask<String> futureTask = new FutureTask<>(task);
Thread thread = new Thread(futureTask);
thread.start();
System.out.println(futureTask.get());
}
}
1.2.4小结
1.3线程常用方法
- 线程等待方法:wait()、wait(long timeout)、wait(long timeout,int nanos)
- 线程通知方法:nodify()、notifyAll()
- 让出优先权:yield()
- 线程中断方法:interrupt()、isinterrupted()、interrupted()
- 线程休眠方法:sleep()
1.3.1线程等待与通知
线程等待方法:
-
wait():当一个线程 A 调用一个共享变量的 wait() 方法时,线程 A 会被阻塞挂起,直到发生下面几种情况才会返回 :
1.1 线程 B 调用了共享对象 notify()或者 notifyAll() 方法;
1.2 其他线程调用了线程 A 的 interrupt()方法,线程 A 抛出 InterruptedException 异常返回。
-
wait(long timeout) :这个方法相比 wait() 方法多了一个超时参数,它的不同之处在于,如果线程 A 调用共享对象的 wait(long timeout)方法后,没有在指定的 timeout 时间内被其它线程唤醒,那么这个方法还是会因为超时而返回。、
-
wait(long timeout, int nanos),其内部调用的是 wait(long timout) 方法。
唤醒/通知线程主要有下面两个方法:
- notify():一个线程 A 调用共享对象的 notify() 方法后,会唤醒一个在这个共享变量上调用 wait 系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。
- notifyAll():不同于在共享变量上调用 notify()方法会唤醒被阻塞到该共享变量上的一个线程,notifyAll 方法会唤醒所有在该共享变量上调用 wait 系列方法而被挂起的线程。
join():等待线程执行终止
- 如果一个线程 A 执行了 thread.join(),当前线程 A 会被阻塞,即等待 thread 线程执行终止之后才从
thread.join()
返回
1.3.2线程睡眠
- sleep(long millis):Thread 类中的静态方法,当一个执行中的线程 A 调用了 Thread 的 sleep 方法后,线程 A 会暂时让出指定时间的执行权。
- 但是线程 A 所拥有的监视器资源,比如锁,还是持有不让出的。指定的睡眠时间到了后该方法会正常返回,接着参与 CPU 的调度,获取到 CPU 资源后就可以继续运行
1.3.3让出CPU执行权
- yield():Thread 类中的静态方法,当一个线程调用 yield 方法时,实际是在暗示线程调度器,当前线程请求让出自己的 CPU
1.3.4线程中断
-
Java 中的线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行。
-
void interrupt():中断线程
2.1 例如:当线程A运行时,线程B可以调用线程A的interrupt()方法来设置线程A的中断标志为true并立即返回
2.2 设置中断标志仅仅是标记,线程A并没有被中断,会继续往下执行
2.3 但如果线程A因为调用wait、join、以及sleep方法而被阻塞挂起,这时线程B若调用线程A的interrupt()方法,线程A会在调用这些方法的地方抛出InterrupedException异常
-
boolean isInterrupted():检测当前线程是否被中断,如果是返回true,否则返回false
-
boolean interrupted():检测当前线程是否被中断,如果是返回true,否则返回false。与 isInterrupted 不同的是,该方法如果发现当前线程被中断,则会清除中断标志。
1.4理解线程上下文切换
- 在多线程编程中,线程个数一般都大于CPU个数,但是每个CPU同一时刻只能被一个线程使用
- 为了让用户感觉多个线程是在同时执行,CPU资源的分配采用了时间片轮转的方法,即给每个线程分配一个时间片,线程在时间片内占用CPU执行任务。当线程使用完时间片,就处于就绪状态并让出CPU让其他线程占用,即上下文切换
1.5线程死锁
1.5.1什么是线程死锁?
-
死锁是指两个或两个以上的线程在执行过程中,因为争夺资源而造成的互相等待的现象
-
产生死锁的四个条件:
2.1 互斥性:资源是互斥的,同一时刻只能由一个线程占用
2.2 请求并持有条件:一个线程已经占有一个资源,同时提出新的资源请求,并占据已有的资源不释放
2.3 不可剥夺条件:线程获取到的资源在自己使用完之前不能被其他线程所占用
2.4 环路等待条件:指在发生死锁时,必然存在一个线程一资源的环形链
-
代码举例:
import java.util.Date; public class LockTest { public static String obj1 = "obj1"; public static String obj2 = "obj2"; public static void main(String[] args) { LockA la = new LockA(); new Thread(la).start(); LockB lb = new LockB(); new Thread(lb).start(); } } class LockA implements Runnable{ public void run() { try { System.out.println(new Date().toString() + " LockA 开始执行"); while(true){ synchronized (LockTest.obj1) { System.out.println(new Date().toString() + " LockA 锁住 obj1"); Thread.sleep(3000); // 此处等待是给B能锁住机会 synchronized (LockTest.obj2) { System.out.println(new Date().toString() + " LockA 锁住 obj2"); Thread.sleep(60 * 1000); // 为测试,占用了就不放 } } } } catch (Exception e) { e.printStackTrace(); } } } class LockB implements Runnable{ public void run() { try { System.out.println(new Date().toString() + " LockB 开始执行"); while(true){ synchronized (LockTest.obj2) { System.out.println(new Date().toString() + " LockB 锁住 obj2"); Thread.sleep(3000); // 此处等待是给A能锁住机会 synchronized (LockTest.obj1) { System.out.println(new Date().toString() + " LockB 锁住 obj1"); Thread.sleep(60 * 1000); // 为测试,占用了就不放 } } } } catch (Exception e) { e.printStackTrace(); } } }
-
执行结果:
1.5.2如何避免死锁?
-
只要破坏产生死锁的四大条件中的一个即可,其中破坏环形等待条件最为容易,即保持资源申请的有序性就可以避免死锁
-
例子:
class LockB implements Runnable{ public void run() { try { System.out.println(new Date().toString() + " LockB 开始执行"); while(true){ synchronized (LockTest.obj1) { System.out.println(new Date().toString() + " LockB 锁住 obj2"); Thread.sleep(3000); synchronized (LockTest.obj2) { System.out.println(new Date().toString() + " LockB 锁住 obj1"); Thread.sleep(60 * 1000); } } } } catch (Exception e) { e.printStackTrace(); } } }