Java多线程笔记
基本概念
程序(program):是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
进程(process):是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。–生命周期
线程(thread):进程可进一步细化为线程,是一个程序内部的一条执行路径。
- 若一个进程同一时间 并行执行多个线程,就是支持多线程的。
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小。
- 一个进程中的多个线程共享相同的内存单元/内存地址空间它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。
何时需要多线程
- 程序需要同时执行两个或多个任务。
- 程序需要实现一些需要等待的任务时,如用户输入、文件读写
操作、网络操作、搜索等。 - 需要一些后台运行的程序时。
线程的创建和使用
方式一:继承Thread类
1)定义子类继承Thread类。
2)子类中重写Thread类中的run方法。
3)创建Thread子类对象,即创建了线程对象。
4)调用线程对象start方法:启动线程,调用run方法。
注意点:
- 如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。
- run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定。
- 想要启动多线程,必须调用start方法。
- 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出非法线程状态的异常"IllegalThreadStateException"。
代码:
public class ThreadTest {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
MyThread thread2 = new MyThread();
thread2.start();
for (int i = 0; i < 1000; i++) {
System.out.println(i+"---- main ----");
}
}
}
class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
方式二:实现Runnable接口
- 创建一个实现Runnable接口的类。
- 在实现类中实现Runnable接口中的run()抽象方法。
- 创建实现类的对象。
- 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象。
- 通过Thread类的对象调用start()
示例:
public class ThreadTest2 {
public static void main(String[] args) {
MyThread thread = new MyThread();
new Thread(thread).start();
}
}
class MyThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
继承Thread方式和实现Runnable方式的联系与区别
区别
-
继承Thread: 线程代码存放Thread子类run方法中。
-
实现Runnable:线程代码存在接口的子类的run方法。
实现Runnable方式的好处(优先选择)
- 避免了单继承的局限性。
- 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。
Thread中常用方法
1.void start()
启动线程,并执行对象的run()方法
2.run()
线程在被调度时执行的操作,通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
3.static currentThread()
返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类
4.getName()/setName()
返回/设置线程的名称
5.static void yield()
线程让步,释放当前CPU的执行权
- 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
- 若队列中没有同优先级的线程,忽略此方法
6.join()
当某个程序执行流中调用其他线程的join()
方法时,调用线程将被阻塞,直到join()
方法加入的join
线程执行完为止。
也就是说,在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完毕以后,线程a才结束阻塞状态。
- 低优先级的线程也可以获得执行
7.stop() "Deprecated"
强制线程生命期结束,不推荐使用
8.static void sleep(long millis) :(指定时间:毫秒)
令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。
就是让当前线程“睡眠”指定的millis毫秒,在指定的millis毫秒时间内,当前线程是阻塞状态。
- 抛出InterruptedException异常
9.boolean isAlive()
返回boolean,判断线程是否还活着
线程的优先级
线程的优先级等级
- MAX_PRIORITY :10
- MIN _PRIORITY :1
- NORM_PRIORITY :5
涉及的方法
- getPriority():返回线程优先值
- setPriority(int newPriority):改变线程的优先级
说明
- 线程创建时继承父线程的优先级
- 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
线程的分类
Java中的线程分为两类:一种是守护线程,一种是用户线程。
- 它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开。
- 守护线程是用来服务用户线程的,通过在start()方法前调用thread.setDaemon(true)可以把一个用户线程变成一个守护线程。
- Java垃圾回收就是一个典型的守护线程。
- 若JVM中都是守护线程,当前JVM将退出。
- 形象理解:兔死狗烹,鸟尽弓藏
线程的生命周期
JDK 中用Thread.State
类定义了线程的几种。
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread
类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的 五种状态:
- 新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
- 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态
- 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
线程的同步
方式一:同步代码块
synchronized(同步监视器){
// 需要被同步的代码;
}
说明:
-
操作共享数据的代码,即为需要被同步的代码。不能包含代码多了,也不能包含代码少了。
-
共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
-
同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
要求:多个线程必须要共用同一把锁。
在实现Runnable接口创建多线程的方式中,可以考虑使用this充当同步监视器。
在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类(类名.class)充当同步监视器。
例:
- 在实现Runnable接口中使用同步代码块
public class SellTicketTest {
public static void main(String[] args) {
SellTickets tickets = new SellTickets();
Thread window1 = new Thread(tickets);
Thread window2 = new Thread(tickets);
Thread window3 = new Thread(tickets);
window1.setName("Window 1");
window2.setName("Window 2");
window3.setName("Window 3");
window1.start();
window2.start();
window3.start();
}
}
class SellTickets implements Runnable {
private int tickets = 1000;
// 同步锁
private Object lock = new Object();
@Override
public void run() {
while (true) {
synchronized (lock) {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + tickets);
tickets--;
} else {
break;
}
}
}
}
}
- 在继承Thread类中使用同步代码块
public class SellTicketTest2 {
public static void main(String[] args) {
Thread window1 = new SellTickets2();
Thread window2 = new SellTickets2();
Thread window3 = new SellTickets2();
window1.setName("Window 1");
window2.setName("Window 2");
window3.setName("Window 3");
window1.start();
window2.start();
window3.start();
}
}
class SellTickets2 extends Thread {
private int tickets = 1000;
// 这里的lock是static的变量
private static Object lock = new Object();
@Override
public void run() {
while (true) {
synchronized (lock) {
/*
* 这行代码如果放在while(true)上方,则会出现问题
* -- 直到while(true)执行完毕,其他的线程才有可能进来,这时票已经卖完了
*/
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + tickets);
tickets--;
} else {
break;
}
}
}
}
}
方式二:同步方法
如果操作共享数据的代码证号完整的声明在一个方法中,可以将此方法声明为同步方法。
同步监视器:
-
非静态的同步方法:this
-
静态的同步方法:类名.class
例:
非静态的同步方法:
public class SellTicketTest3 {
public static void main(String[] args) {
SellTickets3 tickets = new SellTickets3();
Thread window1 = new Thread(tickets);
Thread window2 = new Thread(tickets);
Thread window3 = new Thread(tickets);
window1.setName("Window 1");
window2.setName("Window 2");
window3.setName("Window 3");
window1.start();
window2.start();
window3.start();
}
}
class SellTickets3 implements Runnable {
private int tickets = 100;
@Override
public void run() {
while (hasTicket()) {
}
}
private synchronized boolean hasTicket() {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + tickets);
tickets--;
return true;
} else {
return false;
}
}
}
静态的同步方法:
public class SellTicketTest4 {
public static void main(String[] args) {
Thread window1 = new SellTickets4();
Thread window2 = new SellTickets4();
Thread window3 = new SellTickets4();
window1.setName("Window 1");
window2.setName("Window 2");
window3.setName("Window 3");
window1.start();
window2.start();
window3.start();
}
}
class SellTickets4 extends Thread {
private static int tickets = 100;
@Override
public void run() {
while (hasTicket()) {
}
}
// 同步监视器:SellTickets4.class
private static synchronized boolean hasTicket() {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + tickets);
tickets--;
return true;
} else {
return false;
}
}
}
方式三:Lock锁🔒(JDK 5.0之后)
- 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
- ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制,比较常用的是ReentrantLock,可以显式加锁、释放锁。
使用方法:
- 创建锁:private static ReentrantLock lock = new ReentrantLock();
- 调用锁定方法:lock()
- 调用解锁方法:unlock()
使用时,使用try...finally...
进行包裹,在try中进行上锁,在finally中进行解锁,以保证锁一定会被释放。
例:
import java.util.concurrent.locks.ReentrantLock;
public class SellTicketTest5 {
public static void main(String[] args) {
Thread window1 = new SellTickets5();
Thread window2 = new SellTickets5();
Thread window3 = new SellTickets5();
window1.setName("Window 1");
window2.setName("Window 2");
window3.setName("Window 3");
window1.start();
window2.start();
window3.start();
}
}
class SellTickets5 extends Thread {
private static int tickets = 100;
// 1.创建lock
private static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
// 2.调用锁定方法:lock()
lock.lock();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + tickets);
tickets--;
} else {
break;
}
} finally {
// 3.调用解锁方法:unlock()
lock.unlock();
}
}
}
}
synchronized与Lock的异同
相同点:二者都可以解决线程安全问题
不同点:synchronized机制在执行完相应的同步代码后,自动释放同步监视器(锁);Lock需要手动启动同步(lock()),结束同步也需要手动实现(unlock())
优先使用顺序
Lock ➡ 同步代码块(已经进入了方法体,分配了相应资源)➡ 同步方法(在方法体之外)
单例设计式模式之懒汉式(线程安全)
class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
// 这一行提高了运行效率,在创建instance后,后续的线程可以直接获取instance,而不需要再进入同步代码块进行查看,也就无需等待其他线程是否再执行同步代码块
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
public class SingletonTest {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2);
}
}
死锁的问题
死锁:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
说明:
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
- 使用同步时,要避免出现死锁
解决方法
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步