【Java】多线程
1 概述
并发与并行:
- 并发:指两个或多个事件在同一个时间段内发生。
- 并行:指两个或多个事件在同一时刻发生(同时发生)。
在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行(CUP时间片),只不过是给人的感觉是同时运行,因为分时交替运行的时间非常短。
线程与进程:
- 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
- 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
💎一个程序运行后至少有一个进程,一个进程中可以包含多个线程
Java中提供两种方式实现线程,分别为:继承java.lang.Thread
类与实现java.lang.Runnable
接口
2 继承Thread类
构造方法:
public Thread()
:分配一个新的线程对象public Thread(String name)
:分配一个指定名字的新的线程对象。public Thread(Runnable target)
:分配一个带有指定目标新的线程对象。public Thread(Runnable target,String name)
:分配一个带有指定目标新的线程对象并指定名字。
常用方法:
public String getName()
:获取当前线程名称。public void start()
:导致此线程开始执行; Java虚拟机调用此线程的run方法。public void run()
:此线程要执行的任务在此处定义代码。public static void sleep(long millis)
:使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。public static Thread currentThread()
:返回对当前正在执行的线程对象的引用。
创建并启动多线程步骤:
- 定义Thread类的子类,并重写
run()
方法,该run()
方法的方法体即为线程需要完成的任务,因此把run()方法称为线程执行体。
public class MyThread extends Thread {
@Override
public void run() {
}
}
- 创建Thread子类的实例,即创建了线程对象。
- 调用线程对象的
start()
方法来启动该线程。
public class Test1(){
public static void main(String[] args) {
MyThread mt = new MyThread("线程一");
mt.start();
}
}
3 实现Runnable接口
由于Java不支持多继承(如一个扩展JFrame类的GUI程序不可能再继承Thread类),这时该类就需要实现Runnable接口使其具有多线程功能。
实现Runnable接口的程序会创建一个Thread对象,并将Runnable对象与Thread对象关联。构造方法:
public Thread(Runnable target)
:public Thread(Runnable target, String name)
:
以上两种构造方法参数中都存在Runnable实例,使用构造方法可以将Runnable与Thread实例相关联。
步骤:
- 定义Runnable接口的实现类,并重写该接口的
run()
方法,该run()方法的方法体同样是该线程的线程执行体。
public class MyRunnable implements Runnable{
@Override
public void run() {
}
}
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的
start()
方法来启动线程。
public class Test2(){
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
Thread t = new Thread(mr, "线程二");
t.start();
}
}
通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个执行目标。所有的多线程代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。
在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。
实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的。
4 Thread和Runnable的区别
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
实现Runnable接口比继承Thread类所具有的优势:
- 适合多个相同的程序代码的线程去共享同一个资源。
- 可以避免java中的单继承的局限性。
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
- 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。
5 线程同步
当使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。例如:
/*例如电影院售票问题,总共只有100张票,有多个窗口在同时卖电影票*/
public class Ticket implements Runnable{
private int ticket = 100; //总票数
private int sum = 0; //售出票数
@Override
public void run() {
while (true) {
if (ticket > 0) {//有票可卖
//出票,使用sleep模拟出票时间
try{
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在卖...余票:" + --ticket + ",共售出票数:" + ++sum);
}
}
}
}
public class Test3_Security {
public static void main(String[] args) {
//创建线程任务对象
Ticket ticket = new Ticket();
//创建三个窗口对象
Thread t1 = new Thread(ticket,"窗口1");
Thread t2 = new Thread(ticket,"窗口2");
Thread t3 = new Thread(ticket,"窗口3");
t1.start();
t2.start();
t3.start();
}
}
执行结果:
...
窗口3正在卖...余票:3,共售出票数:104
窗口3正在卖...余票:2,共售出票数:105
窗口1正在卖...余票:0,共售出票数:107
窗口2正在卖...余票:1,共售出票数:106
要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制(synchronized)来解决。
窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。
有三种方法完成同步操作:
- 同步代码块
- 同步方法
- 锁机制
5.1 同步代码块
synchronized
关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。格式:
synchronized(同步锁) {
需要同步操作的代码
}
使用同步代码块解决:
public class Ticket implements Runnable {
Object lock = new Object();
@Override
public void run() {
while (true) {
synchronized (lock) {
//卖票操作
}
}
}
}
执行结果:
...
窗口2正在卖...余票:3,共售出票数:97
窗口3正在卖...余票:2,共售出票数:98
窗口1正在卖...余票:1,共售出票数:99
窗口1正在卖...余票:0,共售出票数:100
5.2 同步方法
使用synchronized
修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。格式:
public synchronized void method(){
可能会产生线程安全问题的代码
}
使用同步方法解决:
ublic class Ticket implements Runnable{
@Override
public void run() { //卖票操作
while (true) {
sellTicket();
}
}
public synchronized void sellTicket(){
//卖票操作
}
}
执行结果:
...
窗口2正在卖...余票:3,共售出票数:97
窗口3正在卖...余票:2,共售出票数:98
窗口1正在卖...余票:1,共售出票数:99
窗口1正在卖...余票:0,共售出票数:100
5.3 Lock锁
java.util.concurrent.locks.Lock
机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
Lock锁也称同步锁,加锁和释放锁方法化了,如下:
public void lock()
:加同步锁。public void unlock()
:释放同步锁。
使用Lock锁解决:
public class Ticket implements Runnable{
Lock lock = new ReentrantLock();
@Override
public void run() { //卖票操作
while (true) {
lock.lock();
//卖票操作
lock.unlock();
}
}
}
执行结果:
...
窗口3正在卖...余票:3,共售出票数:97
窗口3正在卖...余票:2,共售出票数:98
窗口2正在卖...余票:1,共售出票数:99
窗口1正在卖...余票:0,共售出票数:100