Java SE 多线程的介绍及使用
一 多线程的实现方式
1.继承Thread类
创建一个自定义类,继承 Thread
类,然后重写 run()
函数,在 run()
函数中实现业务逻辑。
// 创建自定义类
public class MyThread extends Thread{
@Override
public void run() {
try {
Thread.sleep(1000);
System.out.println("这是子线程呢");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
然后在 main()
方法中实例化该类的对象,最后通过 start()
方法运行子线程。
注意事项:不要将主线程的任务放在子线程之前,否则与单线程无异。
public static void main(String[] args) {
MyThread myThread=new MyThread();
myThread.start();
System.out.println("这是主线程!");
}
优缺点总结:
优点,编码简单,易于理解多线程。
缺点,线程类继承了 Thread 类,无法再继承其他类,不利于程序的扩展。
2.实现Runnable接口
创建一个自定义类,实现 Runnable 接口,实现 run() 方法。在创建 Thread 实例时作为参数传递,然后运行Thread实例的start()方法。
// 创建一个自定类,并且实现 Runable 接口。
// 同时添加一个字段,用来接收另一个线程传递的参数值,并且在 run() 方法中输出。
public class MyThread2 implements Runnable{
public MyThread2(String name){
this.name=name;
}
private String name;
@Override
public void run() {
System.out.println("这是使用另一个子线程的任务");
System.out.println("name="+name);
}
}
然后在主线程中创建该类的实例对象,并且将对象作为参数传递给 Thread ,然后运行 Thread 的 start() 方法。
public static void main(String[] args) {
MyThread2 myThread2=new MyThread2("子线程2");
Thread thread=new Thread(myThread2);
thread.start();
System.out.println("这是主线程!");
}
同时还可以使用匿名内部类的方式来实现。
public static void main(String[] args) {
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("这是匿名内部类的写法");
}
});
thread.start();
System.out.println("这是主线程");
}
优缺点总结:
优点,任务类只实现接口,可以继承其他类,实现其它接口,扩展性强,同时可以接收其他线程传递过来参数。
缺点,没缺点,只是多了一个Runnable 对象。
3.实现Callable接口
继承 Thread 类,和实现 Runnable 接口两种实现多线程的共同问题是,无法直接接收到线程的返回值。
具体实现过程:需要定义一个类,实现 Callable 接口,重写 call() 方法,封装要做的业务,定义要返回的数据。接着把Callable 对象封装成 FutureTask对象。
FutureTask
对象才是真正的任务对象。最后将 FutureTask
对象交给Thread对象,运行 Thread 对象的start() 即可。
// Callable 方法接收一个泛型类型,用来作为返回值的类型
public class MyThread3 implements Callable<Integer>{
public MyThread3(Integer num1,Integer num2){
this.num1=num1;
this.num2=num2;
}
private Integer num1;
private Integer num2;
/**
* 重写call方法,返回两束相加的值
* @return
*/
@Override
public Integer call() throws InterruptedException {
Thread.sleep(1000L);
return num1+num2;
}
}
然后在main函数中进行调用
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread3 myThread3=new MyThread3(4,4);
FutureTask<Integer> futureTask=new FutureTask<>(myThread3);
Thread thread=new Thread(futureTask);
thread.start();
System.out.println("这是主线程");
// 该行代码在子线程任务执行完成时才会执行
System.out.println("输出子线程的计算结果:"+futureTask.get());
}
4.线程Thread中的常用方法
sleep()
,指定线程睡眠指定时间,单位为毫秒。
join()
,等待当前线程执行完成后再执行后面的程序。
wait()
,让线程进入等待,直到它被唤醒,或指定时间后。
二 线程安全问题
多个线程同时操作同一个共享资源
的时候,可能会出现业务安全问题。
综合分析:1.多条线程同时执行;2.同时访问共享资源
1.线程同步
线程同步是解决线程安全问题的解决方案。
线程同步的思想:为对象加锁,保证每次只能有一个线程访问资源,等这个线程执行完毕再解锁,然后再放其他线程进来。
2.加锁方式1-同步代码块
将访问共享资源的代码给上锁,以此保证线程安全。
// 要上锁的对象object
// 底层会记住被加锁的代码块,如果已被加锁,则其他线程无法访问
synchronized (object) {
// 核心业务代码
}
一般情况下,使用共享资源作为锁对象即可,实例方法使用 this ,静态方法时,可使用当前类的类对象加锁。保证加锁的对象在程序中只有一份即可。
原理:每次只允许一个线程加锁,执行完毕后自动解锁,其他线程才能进来执行。
注意事项:对于当前同步执行的线程来说,同步锁必须是同一把,否则会出bug。
3.加锁方式2-同步方法
把访问共享资源的核心方法给上锁,以此保证线程安全。
// 在方法上使用 synchronized 关键字
public synchronized void drawMoney(Double money) throws InterruptedException {
// 核心代码
}
原理和同步代码块原理一样。
4.加锁方式3-Lock锁
通过Lock锁可以创建出一个锁对象,然后自行加锁和解锁,使用起来更加灵活,方便。
Lock锁是一个接口,不能直接实例化,可以采用它的实现类ReentrantLock类
来构建Lock锁。
private Lock lock=new ReentrantLock();
public void functionName(Double money) {
lock.lock();
try {
// 核心代码块
} finally {
lock.unlock();
}
}
5.线程通信
当多个线程共同操作共享的资源时,线程之间可以通过某种方式相互告知自己的状态,相互协调,并避免无效的资源争夺。
线程通信常用模型:生产消费模型
三 其他相关内容
1.线程池
线程池是一个可以复用线程
的技术。
ExecutorService
是JDK5.0开始提供的一个代表线程池的接口。
可以使用 ThreadPoolExecutor 创建线程池对象,也可以使用线程池工具类 Executors 来创建线程池对象。
2.并发和并行
进程:一个正在运行的程序,就是一个独立的进程。
线程是属于进程的,一个进程中可以运行很多个线程。
3.线程的生命周期
Java中,总共定义了6种线程的状态,可以在Thread类的枚举中看到。
以下是线程的6种状态及相互转换。