Java 多线程的使用
多线程的使用
一,继承Thread类
构造方法:
方法名 | 说明 |
---|---|
Thread() | 分配新的Thread对象。 |
Thread(String name) | 分配新的Thread对象,将指定的name作为其线程名称。 |
常用方法:
方法名 | 说明 |
---|---|
void start() | 使该线程开始执行:Java虚拟机调用该线程的run方法。 |
void run() | 该线程要执行的操作。 |
static void sleep(long millis) | 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。 |
创建线程的步骤:
- 定义一个类继承Thread类。
- 重写run方法。
- 创建子类对象,就是创建线程对象。
- 调用start方法,开启线程并让线程执行,同时还会告诉jvm去调用run方法。
public class Demo {
public static void main(String[] args) {
//创建自定义线程对象
MyThread mt = new MyThread("新的线程!");
//开启新线程
mt.start();
//在主方法中执行for循环
for (int i = 0; i < 10; i++) {
System.out.println("main线程!" + i);
}
}
}
//线程类
class MyThread extends Thread {
//定义指定线程名称的构造方法
public MyThread(String name) {
//调用父类的String参数的构造方法,指定线程的名称
super(name);
}
/**
* 重写run方法,完成该线程执行的逻辑
*/
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName() + ":正在执行!" + i);
}
}
}
线程对象调用 run方法和调用start方法区别?线程对象调用run方法不开启线程。仅是对象调用方法。线程对象调用start开启线程,并让jvm调用run方法在开启的线程中执行。
获取线程名称
方法名 | 说明 |
---|---|
Thread.currentThread() | 获取当前线程对象 |
Thread.currentThread().getName(); | 获取当前线程对象的名称 |
二,实现Runnable接口
接口中的方法:
方法名 | 说明 |
---|---|
void run() | 使用实现接口Runnable的对象创建一个线程时,启动该线程将导致在独立执行中的线程调用对象的run方法。 |
创建线程的的步骤:
- 定义类实现Runnable接口。
- 覆盖接口中的Run方法。
- 创建Thread类的对象。
- 将Runnable接口的子类对象作为参数传递给Thread类的构造函数。
- 调用Thread类的start方法开启线程。
public class Demo {
public static void main(String[] args) {
//创建线程执行目标类对象
Runnable runn = new MyRunnable();
//将Runnable接口的子类对象作为参数传递给Thread类的构造函数
Thread thread = new Thread(runn);
Thread thread2 = new Thread(runn);
//开启线程
thread.start();
thread2.start();
for (int i = 0; i < 10; i++) {
System.out.println("main线程:正在执行!" + i);
}
}
}
//自定义线程执行任务类
class MyRunnable implements Runnable {
//定义线程要执行的run方法逻辑
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("我的线程:正在执行!" + i);
}
}
}
线程池的使用方法
线程池,其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
我们详细的解释一下为什么要使用线程池?
在java中,如果每个请求到达就创建一个新线程,开销是相当大的。在实际使用中,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。为了防止资源不足,需要采取一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务。
线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使用应用程序响应更快。另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。
一,使用Runnable接口
通常,线程池都是通过线程池工厂创建,再调用线程池中的方法获取线程,再通过线程去执行任务方法。
Executors:线程池创建工厂类
方法名 | 说明 |
---|---|
public static ExecutorService newFixedThreadPool(int nThreads) | 返回线程池对象 |
ExecutorService:线程池类
方法名 | 说明 |
---|---|
Future<?> submit(Runnable task) | 获取线程池中的某一个线程对象,并执行 |
Future接口 | 用来记录线程任务执行完毕后产生的结果。线程池创建与使用 |
使用线程池中线程对象的步骤:
- 创建线程池对象
- 创建Runnable接口子类对象
- 提交Runnable接口子类对象
- 关闭线程池
package thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
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();
}
}
//Runnable接口实现类
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("教我游泳,交完后,教练回到了游泳池");
}
}
二,使用Callable接口
Callable接口:与Runnable接口功能相似,用来指定线程的任务。其中的call()方法,用来返回线程任务执行完毕后的结果,call方法可抛出异常。
ExecutorService:线程池类
方法名 | 说明 |
---|---|
获取线程池中的某一个线程对象,并执行线程中的call()方法 |
Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用
- 使用线程池中线程对象的步骤:
- 创建线程池对象
- 创建Callable接口子类对象
- 提交Callable接口子类对象
- 关闭线程池
package cn.last.demo;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SpringTestApplicationTests {
public static void main(String[] args) {
//创建线程池对象
ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
//创建Callable对象
MyCallable c = new MyCallable();
//从线程池中获取线程对象,然后调用MyRunnable中的run()
service.submit(c);
//再获取个教练
service.submit(c);
service.submit(c);
//注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。将使用完的线程又归还到了线程池中
//关闭线程池
//service.shutdown();
}
}
//Callable接口实现类,call方法可抛出异常、返回线程任务执行完毕后的结果
class MyCallable implements Callable<Object> {
@Override
public Object call() throws Exception {
System.out.println("我要一个教练:call");
Thread.sleep(2000);
System.out.println("教练来了: " + Thread.currentThread().getName());
System.out.println("教我游泳,交完后,教练回到了游泳池");
return null;
}
}
三,线程池练习:返回两个数相加的结果
要求:通过线程池中的线程对象,使用Callable接口完成两个数求和操作
- Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用
- V get() 获取Future对象中封装的数据结果
package cn.last.demo;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class SpringTestApplicationTests {
public static void main(String[] args) throws InterruptedException, ExecutionException {
//创建线程池对象
ExecutorService threadPool = Executors.newFixedThreadPool(2);
//创建一个Callable接口子类对象
//MyCallable c = new MyCallable();
MyCallable c = new MyCallable(100, 200);
MyCallable c2 = new MyCallable(10, 20);
//获取线程池中的线程,调用Callable接口子类对象中的call()方法, 完成求和操作
//<Integer> Future<Integer> submit(Callable<Integer> task)
// Future 结果对象
Future<Integer> result = threadPool.submit(c);
//此 Future 的 get 方法所返回的结果类型
Integer sum = result.get();
System.out.println("sum=" + sum);
//再演示
result = threadPool.submit(c2);
sum = result.get();
System.out.println("sum=" + sum);
//关闭线程池(可以不关闭)
}
}
//Callable接口实现类
class MyCallable implements Callable<Integer> {
//成员变量
int x;
int y;
public MyCallable(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public Integer call() {
return x + y;
}
}
线程同步(线程安全处理Synchronized)
java中提供了线程同步机制,它能够解决上述的线程安全问题。
线程同步的方式有两种:
一,同步代码块
同步代码块: 在代码块声明上 加上synchronized
synchronized (锁对象) {
可能会产生线程安全问题的代码
}
同步代码块中的锁对象可以是任意的对象;但多个线程时,要使用同一个锁对象才能够保证线程安全。
使用同步代码块,对电影院卖票案例中Ticket类进行如下代码修改:
package cn.last.demo;
//测试类
public class SpringTestApplicationTests {
public static void main(String[] args) {
//创建票对象
Ticket ticket = new Ticket();
//创建3个窗口
Thread t1 = new Thread(ticket, "窗口1");
Thread t2 = new Thread(ticket, "窗口2");
Thread t3 = new Thread(ticket, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
//模拟票
class Ticket implements Runnable {
//共100票
int ticket = 100;
//定义锁对象
Object lock = new Object();
@Override
public void run() {
//模拟卖票
while (true) {
//同步代码块
synchronized (lock) {
if (ticket > 0) {
//模拟电影选坐的操作
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
}
}
}
}
}
二,同步方法
同步方法:在方法声明上加上synchronized
public synchronized void method(){
可能会产生线程安全问题的代码
}
同步方法中的锁对象是 this
使用同步方法,对电影院卖票案例中Ticket类进行如下代码修改:
三,静态同步方法: 在方法声明上加上static synchronized
public static synchronized void method(){
可能会产生线程安全问题的代码
}
静态同步方法中的锁对象是 类名.class
四,死锁
同步锁使用的弊端:当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步。这时容易引发一种现象:程序出现无限等待,这种现象我们称为死锁。这种情况能避免就避免掉。
synchronzied(A锁){
synchronized(B锁){
}
}
我们进行下死锁情况的代码演示:
package cn.last.demo;
import java.util.Random;
//定义锁对象类
class MyLock {
public static final Object lockA = new Object();
public static final Object lockB = new Object();
}
//测试类
public class SpringTestApplicationTests {
public static void main(String[] args) {
//创建线程任务类对象
ThreadTask task = new ThreadTask();
//创建两个线程
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
//启动线程
t1.start();
t2.start();
}
}
//线程任务类
class ThreadTask implements Runnable {
int x = new Random().nextInt(1);//0,1
//指定线程要执行的任务代码
@Override
public void run() {
while (true) {
if (x % 2 == 0) {
//情况一
synchronized (MyLock.lockA) {
System.out.println("if-LockA");
synchronized (MyLock.lockB) {
System.out.println("if-LockB");
System.out.println("if大口吃肉");
}
}
} else {
//情况二
synchronized (MyLock.lockB) {
System.out.println("else-LockB");
synchronized (MyLock.lockA) {
System.out.println("else-LockA");
System.out.println("else大口吃肉");
}
}
}
x++;
}
}
}
五,Lock接口
查阅API,查阅Lock接口描述,Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。
Lock接口中的常用方法:
方法名 | 说明 |
---|---|
void lock() | 获取锁 |
void unlock() | 释放锁 |
Lock提供了一个更加面对对象的锁,在该锁中提供了更多的操作锁的功能。
我们使用Lock接口,以及其中的lock()方法和unlock()方法替代同步,对电影院卖票案例中Ticket类进行如下代码修改:
package cn.last.demo;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Ticket implements Runnable {
//共100票
int ticket = 100;
//创建Lock锁对象
Lock ck = new ReentrantLock();
@Override
public void run() {
//模拟卖票
while (true) {
//synchronized (lock){
ck.lock();
if (ticket > 0) {
//模拟选坐的操作
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
}
ck.unlock();
}
}
}
六,等待唤醒机制
在开始讲解等待唤醒机制之前,有必要搞清一个概念——线程之间的通信:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。
等待唤醒机制所涉及到的方法:
方法名 | 说明 |
---|---|
void notify() | 唤醒,唤醒线程池中被wait()的线程,一次唤醒一个,而且是任意的。 |
void notifyAll() | 唤醒全部:可以将线程池中的所有wait() 线程都唤醒。 |
void wait() | 等待,将正在执行的线程释放其执行资格 和 执行权,并存储到线程池中。 |
其实,所谓唤醒的意思就是让 线程池中的线程具备执行资格。必须注意的是,这些方法都是在 同步中才有效。同时这些方法在使用时必须标明所属锁,这样才可以明确出这些方法操作的到底是哪个锁上的线程。
仔细查看JavaAPI之后,发现这些方法 并不定义在 Thread中,也没定义在Runnable接口中,却被定义在了Object类中,为什么这些操作线程的方法定义在Object类中?
因为这些方法在使用时,必须要标明所属的锁,而锁又可以是任意对象。能被任意对象调用的方法一定定义在Object类中。