[Java基础]8. Java多线程基础
文章目录
一、进程与线程
进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
线程是进程中的一个执行单位,负责当前进程中程序的执行。一个进程中至少有一个线程,多个线程的进程运用程序就叫做多线程程序。
多线程就是多个线程同时运行(多核CPU)或交替运行(单核CPU)。
线程优先级具有继承特性比如A线程启动B线程,则B线程的优先级和A是一样的。
线程优先级具有随机性也就是说线程优先级高的不一定每一次都先执行完,只是被执行的可能性更大。
使用多线程而不是用多进程去进行并发程序的设计,是因为线程间的切换和调度的成本远远小于进程。多线程会将程序运行方式从串行运行变为并发运行
- 并发:一个时间段内同时发生(并不是同时发生)。如单核CPU:交替运行
- 并行:同一时刻发生(真正的同时发生)。如多核CPU:同时运行
主线程(Main线程)的执行过程:JVM执行main方法,main方法进入栈内存,JVM会找操作系统开辟一条main方法通向cpu的执行路径,cpu通过这个路径来执行main方法,这个路径即main(主)线程。
二、Java线程实现
(一)继承Thread类创建线程类
Java使用java.lang.Thread
类代表线程,所有的线程对象都必须是Thread类或其子类的实例。
①定义类继承Thread
类,并重写Thread类的run()
方法,该run()
方法的方法体就代表了线程需要完成的任务。因此把run()
方法称为线程执行体。
②创建Thread
子类的实例,即创建了线程对象。
③调用线程对象的start()
方法来启动该线程。
public class ExtendsThreadDemo {
public static void main(String[] args) {
MyThread1 thread1 = new MyThread1();
MyThread1 thread2 = new MyThread1();
// 调用start方法方可启动线程
// 不能调用run()方法,run方法只是thread的一个普通方法,还是在主线程里执行。
thread1.start();
thread2.start();
System.out.println("‐‐‐‐‐‐‐‐‐‐‐‐‐‐");
}
}
class MyThread1 extends Thread {
@Override
public void run() {
System.out.println("线程的id:" + getId());
System.out.println("线程的名称:" + getName());
for (int i = 0; i < 50; i++) {
System.out.println("线程的id:" + getId() + " 线程的名称:" + getName());
}
}
}
Thread类本质上是实现了Runnable
接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过Thread类的start()
实例方法。start()
方法是一个native
方法,它将启动一个新线程,并执行run()
方法。
(二)实现Runnable接口创建线程类
如果自己的类已经继承另一个类,就无法直接继承Thread,此时,可以实现一个Runnable
接口来创建线程
①定义类实现Runnable
接口,并重写Runnable
接口的run()
方法,该run()
方法的方法体就代表了线程需要完成的任务。
②创建Runnable实现类
的实例,并以此实例作为Thread的target来创建Thread
对象,该Thread对象才是真正的线程对象。
③调用该Thread对象的start()
方法来启动该线程。
public class RunnableThreadDemo {
public static void main(String[] args) {
//创建Runnable实现类的实例
MyThread2 myThread = new MyThread2();
//创建Thread类,并把Runnable实现类的实例作为参数
Thread thread1 = new Thread(myThread);
Thread thread2 = new Thread(myThread);
//start() 方法启动线程
thread1.start();
thread2.start();
}
}
class MyThread2 implements Runnable {
@Override
public void run() {
// 实现Runnable接口的,无法直接使用getId(),getName()等方法
// 需要使用Thread.currentThread() 来获取到当前对象才行
System.out.println("线程的id:" + Thread.currentThread().getId());
System.out.println("线程的名称:" + Thread.currentThread().getName());
for (int i = 0; i < 50; i++) {
System.out.println("线程的id:" + Thread.currentThread().getId() + " 线程的名称:" + Thread.currentThread().getName());
}
}
}
(三)实现Callable
接口通过FutureTask
包装器来创建Thread
线程
(四)使用ExecutorService
、Callable
、Future
实现有返回结果的线程。
两种入门级创建线程的区别
采用继承Thread类方式:
(1)优点:编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this,即可获得当前线程。
(2)缺点:因为线程类已经继承了Thread类,所以不能再继承其他的父类。
采用实现Runnable接口方式:
(1)优点:线程类只是实现了Runable接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
(2)缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。
实现Runnable接口比继承Thread类的优势:
1.适合多个相同代码的线程去处理同一个资源。
2.可以避免java中单继承的限制。
3.增加代码的健壮性,实现解耦。代码可以被多个线程共享,代码和数据独立。
4.线程池中只能放入实现Runnable或Callable类线程,不能放入继承Thread的类
实际上面两种创建线程的方法很少使用,一般都是用线程池的方式,使用线程池的方式也是最推荐的一种方式。
使用匿名内部类方式创建线程
package AnonymousInner;
public class NiMingInnerClassThread {
public static void main(String[] args) {
Runnable r = new Runnable() {
@Override
public void run() {
for (int i = 0; i<5;i++){
System.out.println("熊孩子:"+i);
}
}
};
new Thread(r).start();
for (int i = 0; i < 5 ; i++){
System.out.println("傻狍子:"+i);
}
}
}
三、线程安全
线程安全问题的解决方法有三种:
1、同步代码块
2、同步方法
3、锁机制
引例
线程安全问题主要是共享资源竞争的问题,也就是在多个线程情况下,一个或多个线程同时抢占同一资源导致出现的一些不必要的问题。
//实例化三个Thread,并传入同一个RunableDemo 实例作为参数,最后开启三条相同参数的线程
public class RunableDemo implements Runnable{
public int a = 100;//线程共享数据
@Override
public void run() {
while (a>0){
System.out.println("线程"+Thread.currentThread().getName()+"执行到"+ a);
a--;
}
}
}
public class MainThreadDemo {
public static void main(String[] args) {
RunableDemo runn = new RunableDemo();
Thread thread1 = new Thread(runn);
Thread thread2 = new Thread(runn);
Thread thread3 = new Thread(runn);
thread1.start();
thread2.start();
thread3.start();
}
}
Thread-0==100
Thread-0==99
Thread-1==100
Thread-1==97
Thread-1==96
Thread-1==95
Thread-2==98
...
安全问题就出在线程会出现相同的结果
1. 使用synchronized同步代码块
synchronized(锁对象) {
可能会出现线程安全问题的代码(访问共享数据的代码)
}
注意:
- 通过代码块的锁对象,可以是任意对象
- 必须保证多个线程使用的锁对象必须是同一个
- 锁对象的作用是把同步代码快锁住,只允许一个线程在同步代码块执行
public class RunableDemo implements Runnable{
public int a = 100;//线程共享数据
Object o=new Object(); //事先准备好一个锁对象,同步监视器
@Override
public void run() {
synchronized (o){ //使用同步代码块,任何线程要执行下面的代码,必须要拿到o的锁
while (a>0){
System.out.println("线程"+Thread.currentThread().getName()+"执行到"+ a);
a--;
}
}
}
}
修改后数据都变成正确了。
o指向的是堆内存中的Object对象,
synchronized (o)
并不是锁o,也不是锁住下面的代码块,而是锁住堆中o指向的对象,即当前对象。
同步代码块的原理:
使用了一个锁对象,叫同步锁,对象锁,也叫同步监视器,当开启多个线程的时候,多个线程就开始抢夺CPU的执行权,比如现在t0线程首先的到执行,就会开始执行run方法,遇到同步代码快,首先检查是否有锁对象,发现有,则获取该锁对象,执行同步代码块中的代码。之后当CUP切换线程时,比如t1得到执行,也开始执行run方法,但是遇到同步代码块检查是否有锁对象时发现没有锁对象,t1便被阻塞,等待t0执行完毕同步代码块,释放锁对象,t1才可以获取从而进入同步代码块执行。
同步中的线程,没有执行完毕是不会释放锁的,这样便实现了线程对临界区的互斥访问,保证了共享数据安全。
缺点:频繁的获取释放锁对象,降低程序效率
2. 同步方法
使用步骤:
1、把访问了共享数据的代码抽取出来,放到一个方法中
2、在该方法上添加synchronized
修饰符
修饰符 synchronized 返回值类型 方法名称(参数列表) {
方法体...
}
public class RunableDemo implements Runnable{
public int a = 100;//线程共享数据
@Override
public void run() {
while (true){
sell(); //调用下面的sell方法
}
}
//访问了共享数据的代码抽取出来,放到一个方法sell中
public synchronized void sell(){
while (a>0){
System.out.println("线程"+Thread.currentThread().getName()+"执行到"+ a);
a--;
}
}
}
同步方法的也是一样锁住同步的代码,但是锁对象的是Runable实现类对象,也就是this,谁调用方法,就是谁。即同步方法的同步监视器就是this,也就是调用该方法的对象。
静态同步方法:在同步方法上加上静态static
修饰符,此时锁对象就不是this了,静态同步方法的锁对象是本类的class属性,class文件对象(反射)
public class RunableDemo implements Runnable{
public static int a = 100;//线程共享数据 =====此时共享数据也要加上static
@Override
public void run() {
while (true){
sell();
}
}
public static synchronized void sell(){ //注意添加了static关键字
while (a>0){
System.out.println("线程"+Thread.currentThread().getName()+"执行到"+ a);
a--;
}
}
}
3. Lock锁
Lock接口位于java.util.concurrent.locks.Lock
它是JDK1.5之后出现的,Lock接口中的方法:
void lock()
: 获取锁
void unlock()
: 释放锁
Lock接口的一个实现类java.util.concurrent.locks.ReentrantLock implements Lock
接口
使用方法:
1、在Runable
实现类的成员变量创建一个ReentrantLock
对象
2、在可能产生线程安全问题的代码前该对象调用lock
方法获取锁
3、在可能产生线程安全问题的代码后该对象调用unlock
方法释放锁
import java.util.concurrent.locks.ReentrantLock;
public class RunableDemo implements Runnable{
public static int a = 100;//线程共享数据
//1、在Runable实现类的成员变量创建一个ReentrantLock对象============
ReentrantLock reentrantLock = new ReentrantLock();
@Override
public void run() {
// 2、在可能产生线程安全问题的代码前该对象调用lock方法获取锁=======
reentrantLock.lock();
while (a>0){
System.out.println("线程"+Thread.currentThread().getName()+"执行到"+ a);
a--;
}
// 3、在可能产生线程安全问题的代码后该对象调用unlock方法获取锁======
reentrantLock.unlock();
}
}
当然更安全的写法是,在线程安全问题代码中try...catch
,最后在finally
语句中添加reentrantLock.unlock()
;
4. 三种方法总结
第一种
synchronized 同步代码块:可以是任意的对象必须保证多个线程使用的锁对象是同一个
第二种
synchronized 同步方法: 锁对象是this,谁调用锁对象就是谁
synchronized 静态同步方法: 锁对象是其class对象,该对象可以用
this.getClass()
方法获取,也可以使用当前类名.class
表示。【了解即可】
第三种
Look锁方法:该方法提供的方法远远多于synchronized
方式,主要在Runable实现类的成员变量创建一个ReentrantLock
对象,并使用该对象调用lock
方法获取锁以及unlock
方法释放锁!
四、Thread类常用基本方法
构造器/方法 | 说明 | 返回值 |
---|---|---|
Thread() | 分配新的 Thread 对象。 | |
Thread(Runnable target) | ||
Thread(Runnable target, String name) | ||
currentThread() | 返回对当前正在执行的线程对象的引用。 | static Thread |
sleep(long millis) | 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。 | static void |
sleep(long millis, int nanos) | static void | |
yield() | 暂停当前正在执行的线程对象,并执行其他线程。 | static void |
start() | 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。 | void |
getId() | 返回该线程的标识符。 | long |
getName() | 返回该线程的名称。 | String |
getPriority() | 返回线程的优先级。 | int |
isAlive() | 测试线程是否处于活动状态。 | boolean |
join() | 等待该线程终止。 | void |
join(long millis) | 等待该线程终止的时间最长为 millis 毫秒。 | void |
join(long millis, int nanos) | void | |
setName(String name) | 改变线程名称,使之与参数 name 相同。 | void |
setPriority(int newPriority) | 更改线程的优先级。参数范围[1,10] | void |
1. Thread类
Thread()
:用于构造一个新的Thread。
Thread(Runnable target)
:用于构造一个新的Thread,该线程使用了指定target的run方法。
Thread(ThreadGroup group,Runnable target)
:用于在指定的线程组中构造一个新的Thread,该
线程使用了指定target的run方法。
currentThread()
:获得当前运行线程的对象引用。
interrupt()
:将当前线程置为中断状态。
sleep(long millis)
:使当前运行的线程进入睡眠状态,睡眠时间至少为指定毫秒数。
join()
:等待这个线程结束,即在一个线程中调用other.join(),将等待other线程结束后才继续本线程。
yield()
:当前执行的线程让出CPU的使用权,从运行状态进入就绪状态,让其他就绪线程执行。
2. Object类
wait()
:释放当前的锁,让出CPU,让当前线程进入等待阻塞状态,直到其他线程调用了此对象的notify()或notifyAll()方法后,当前线程才被唤醒进入就绪状态。
notify()
:唤醒在此对象监控器(锁对象)上等待的单个线程。
notifyAll()
:唤醒在此对象监控器(锁对象)上等待的所有线程。
注意:
- wait()、notify()、notifyAll()都依赖于同步锁,而同步锁是对象持有的,且每个对象只有一个,所以这些方法定义在
Object
类中,而不是Thread
类中- wait()、notify/notifyAll()方法是Object的
final
方法,无法被重写- 一般在
synchronized
同步代码块里使用wait()、notify/notifyAll()方法。- notify/notifyAll()的执行只是唤醒沉睡的线程,而不会立即释放锁,会继续往下执行,直到执行完synchronized代码块的代码或是中途遇到wait(),再次释放锁。所以在编程中,尽量在使用了notify/notifyAll()后立即退出临界区,以唤醒其他线程。
wait()
需要被try…catch包围。- notify和wait的顺序不能错,如果A线程先执行notify方法,B线程在执行wait方法,那么B线程是无法被唤醒的。
3. yield()、sleep()、wait()比较
wait()
:让线程从运行状态进入等待阻塞状态,并且会释放它所持有的同步锁。
yield()
:让线程从运行状态进入就绪状态,不会释放它锁持有的同步锁。
sleep()
:让线程从运行状态进入阻塞状态,不会释放它锁持有的同步锁。
4. 例子
/**
* wait()、notify()方法演示
*/
public class NotifyDemo {
public static void main(String[] args) {
final Printer p = new Printer();
new Thread() {
public void run() {
while (true) {
p.printer1();
}
}
}.start();
new Thread() {
public void run() {
while (true) {
p.printer2();
}
}
}.start();
}
}
class Printer {
private int flag=1;
public void printer1() {
synchronized (this) {
if(flag!=1){
try {
//此线程等待
this.wait();
} catch (InterruptedException e) {
// TODO Auto‐generated catch block
e.printStackTrace();
}
}
System.out.print("1");
System.out.print("1");
System.out.print("1");
System.out.print("1");
System.out.print("1");
System.out.print("1");
System.out.print("1");
System.out.print("1");
System.out.print("\r\n");
//设置标志为2,唤醒另外一个线程
flag=2;
this.notify();
}
}
public void printer2() {
synchronized (this) {
if (flag!=2) {
try {
//此线程等待
this.wait();
} catch (InterruptedException e) {
// TODO Auto‐generated catch block
e.printStackTrace();
}
}
System.out.print("2");
System.out.print("2");
System.out.print("2");
System.out.print("2");
System.out.print("2");
System.out.print("2");
System.out.print("2");
System.out.print("2");
System.out.print("\r\n");
//设置标志为1,唤醒另外一个线程
flag=1;
this.notify();
}
}
}
//结果是11111111和22222222交替打印
/**
* wait()、notifyAll()演示
*/
public class NotifyAllDemo {
public static void main(String[] args) {
final PrinterAll p = new PrinterAll();
new Thread() {
public void run() {
while (true) {
p.printer1();
}
}
}.start();
new Thread() {
public void run() {
while (true) {
p.printer2();
}
}
}.start();
new Thread() {
public void run() {
while (true) {
p.printer3();
}
}
}.start();
}
}
class PrinterAll {
private int flag = 1;
public void printer1() {
synchronized (this) {
while (flag != 1) {
try {
this.wait();
} catch (InterruptedException e) {
// TODO Auto‐generated catch block
e.printStackTrace();
}
}
System.out.print("1");
System.out.print("1");
System.out.print("1");
System.out.print("1");
System.out.print("1");
System.out.print("\r\n");
flag = 2;
this.notifyAll();
}
}
public void printer2() {
synchronized (this) {
while (flag != 2) {
try {
this.wait();
} catch (InterruptedException e) {
// TODO Auto‐generated catch block
e.printStackTrace();
}
}
System.out.print("2");
System.out.print("2");
System.out.print("2");
System.out.print("2");
System.out.print("2");
System.out.print("\r\n");
flag = 3;
this.notifyAll();
}
}
public void printer3() {
synchronized (this) {
while (flag != 3) {
try {
this.wait();
} catch (InterruptedException e) {
// TODO Auto‐generated catch block
e.printStackTrace();
}
}
System.out.print("3");
System.out.print("3");
System.out.print("3");
System.out.print("3");
System.out.print("3");
System.out.print("\r\n");
flag = 1;
this.notifyAll();
}
}
}
//11111、22222、33333交替出现
五、线程的状态
六、线程池
Java里面线程池的最顶级接口是java.util.concurrent.Executor
,但是严格意义上讲Executor
并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService
。官方建议使用Executors工程类来创建线程池对象。
Executors类中有个创建线程池的方法如下:
public static ExecutorService newFixedThreadPool(int nThreads)
:返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)
使用线程池对象的方法如下:
public Future<?> submit(Runnable task)
:获取线程池中的某一个线程对象,并执行。
Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。
使用线程池中线程对象的步骤:
- 创建线程池对象。
- 创建Runnable接口子类对象。(task)
- 提交Runnable接口子类对象。(take task)
- 关闭线程池(一般不操作这一步)。
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("我要一个游泳教练");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("教练来了: " + Thread.currentThread().getName());
System.out.println("教我游泳,教会后,教练又回到了游泳池");
}
}
线程池测试类:
public class ThreadPoolDemo {
public static void main(String[] args) {
// 创建线程池对象
ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
// 创建Runnable实例对象
MyRunnable r = new MyRunnable();
//自己创建线程对象的方式
// Thread t = new Thread(r);
// t.start(); ---> 调用MyRunnable中的run()
// 从线程池中获取线程对象,然后调用MyRunnable中的run()
service.submit(r);
// 再获取个线程对象,调用MyRunnable中的run()
service.submit(r);
service.submit(r);
// 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。
// 将使用完的线程又归还到了线程池中
// 关闭线程池
//service.shutdown();
}
}