Java之线程
创建线程
有两种方法可以创建线程:
- 需要从Java.lang.Thread类派生一个新的线程类,重载它的run()方法
- 实现Runnable接口,重载Runnable接口中的run()方法
为什么要提供两种方法来创建线程呢?
在Java中,类仅支持单继承,也就是说,当定义一个新的类的时候,它只能扩展一个外部类.这样,如果创建自定义线程类的时候是通过扩展 Thread类的方法来实现的,那么这个自定义类就不能再去扩展其他的类,也就无法实现更加复杂的功能。因此,如果自定义类必须扩展其他的类,那么就可以使用实现Runnable接口的方法来定义该类为线程类,这样就可以避免Java单继承所带来的局限性。另外,使用实现Runnable接口的方式创建的线程可以处理同一资源,从而实现资源的共享。
Runnable这个接口只有一个方法:public void run();
通过Runnable接口创建线程
- 建立Runnable对象
Runnable threaJob=new MyRunnable();
建立Thread对象并赋值Runnable任务
- Thread就像一个工人,需要一个任务才能进行工作,而这个任务就在Runnable()的run()方法中
Thread myThread new Thread(threadJob);
启动Thread
- 工人有了任务滞后,就可以让他去工作了
myThread.start();
例子:
1 | //每个任务都是Runable接口的一个实例,任务是可运行对象,线程是便于任务执行的对象。必须创建任务类,重写run方法定义任务 |
运行结果:
火箭发射倒计时: $Thread-0(9) $Thread-0(8) $Thread-0(7) $Thread-0(6) $Thread-0(5) $Thread-0(4) $Thread-0(3) $Thread-0(2) $Thread-0(1) $Thread-0(0)
如果同时运行两个任务对象,体会其不同的之处:
1 | public static void main(String[] args) { |
运行结果:
火箭发射倒计时: $Thread-0(9) $Thread-0(8) $Thread-0(7) $Thread-0(6) $Thread-1(9) $Thread-0(5) $Thread-1(8) $Thread-0(4) $Thread-1(7) $Thread-0(3) $Thread-1(6) $Thread-1(5) $Thread-0(2) $Thread-1(4) $Thread-1(3) $Thread-1(2) $Thread-1(1) $Thread-1(0) $Thread-0(1) $Thread-0(0)
继承Thread类来创建线程
- 首先创建一个任务类extends Thread类,因为Thread类实现了Runnable接口,所以自定义的任务类也实现了Runnable接口,重写run()方法,其中定义具体的任务代码或处理逻辑
- 创建一个任务类对象,可以用Thread或者Runnable作为自定义的变量类型
- 调用自定义对象的start()方法,启动一个线程
例子:
1 | //每个任务都是Runable接口的一个实例,任务是可运行对象,线程即可运行对象。必须创建任务类,重写run方法定义任务 |
运行结果:
火箭发射倒计时: $Thread-0(9) $Thread-0(8) $Thread-0(7) $Thread-0(6) $Thread-0(5) $Thread-0(4) $Thread-0(3) $Thread-0(2) $Thread-0(1) $Thread-0(0) $Thread-1(9) $Thread-1(8) $Thread-1(7) $Thread-1(6) $Thread-1(5) $Thread-1(4) $Thread-1(3) $Thread-1(2) $Thread-1(1) $Thread-1(0)
两种创建方法的比较
- 使用实现Runnable接口方式创建线程可以共享同一个目标对象,实现了多个相同线程处理同一份资源,即一个线程对资源的操作会影响到另一个进程的操作
- 而继承Thread创建线程的方式,new出了两个任务类对象,有各自的成员变量,相互之间不干扰。
各自的优缺点
- 采用继承Thread类方式:
- 优点:编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this,即可获得当前线程
- 缺点:因为线程类已经继承了Thread类,所以不能再继承其他的父类
- 采用实现Runnable接口方式:
- 优点:线程类只是实现了Runable接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想
- 缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法
本文以上主要内容转载于:java多线程总结一:线程的两种创建方式及比较
线程睡眠
直接上代码:
1 | public class MyRunnable implements Runnable { |
线程同步机制
为什么要线程同步?我们现在知道,可以实现多个线程共享同一个资源(变量或者对象)的情况,如果这些线程都对需要对资源进行读或者写操作,由于线程之间的调度等其它问题,会导致资源的状态出现紊乱,导致程序异常。举个例子:如果一个银行账户同时被两个线程操作,一个取100块,一个存钱100块。假设账户原本有0块,如果取钱线程和存钱线程同时发生,会出现什么结果呢?取钱不成功,账户余额是100.取钱成功了,账户余额是0.那到底是哪个呢?由于是同时发生的两件事,很难说清楚。因此多线程同步就是要解决这个问题。
Java的同步机制是通过对象锁来实现的:
一段synchronized的代码被一个线程执行之前,他要先拿到执行这段代码的权限,在 Java里边就是拿到某个同步对象的锁(一个对象只有一把锁); 如果这个时候同步对象的锁被其他线程拿走了,他(这个线程)就只能等了(线程阻塞在锁池 等待队列中)。 取到锁后,他就开始执行同步代码(被synchronized修饰的代码);线程执行完同步代码后马上就把锁还给同步对象,其他在锁池中 等待的某个线程就可以拿到锁执行同步代码了。这样就保证了同步代码在统一时刻只有一个线程在执行。
例子:
1 | public class ThreadTest2 extends Thread { |
该程序通过在main方法启动10个线程之前,创建了一个String类型的对象。并通过ThreadTest2的构造函数,将这个对象赋值 给每一个ThreadTest2线程对象中的私有变量lock。根据Java方法的传值特点,我们知道,这些线程的lock变量实际上指向的是堆内存中的 同一个区域,即存放main函数中的lock变量的区域。
run方法加一个synchronized块来实现。这个同步块的对象锁,就是 main方法中创建的那个String对象。换句话说,他们指向的是同一个String类型的对象,对象锁是共享且唯一的!
于是,我们看到了预期的效果:10个线程不再是争先恐后的报数了,而是一个接一个的报数。
同步的实现方式可分为:
- 同步方法
- 同步代码块
- 使用特殊域变量(volatile)实现线程同步
- 使用重入锁实现线程同步
- 详细介绍链接
多线程的使用场景
- 为了不阻塞主线程,启动其他线程来做耗时的事情。比如app开发中耗时的操作都不在UI主线程中做。
- 实现响应更快的应用程序, 即主线程专门监听用户请求,子线程用来处理用户请求。以获得大的吞吐量。
感觉这种情况下,多线程的效率未必高。 这种情况下的多线程是为了不必等待, 可以并行处理多条数据。比如JavaWeb的就是主线程专门监听用户的HTTP请求,然后启动子线程去处理用户的HTTP请求。 - 某种优先级虽然很低的服务,但是却要不定时去做。比如Jvm的垃圾回收。
- 某种任务,虽然耗时,但是不耗CPU的操作时,开启多个线程,效率会有显著提高。
比如读取文件,然后处理。 磁盘IO是个很耗费时间,但是不耗CPU计算的工作。 所以可以一个线程读取数据,一个线程处理数据。肯定比一个线程读取数据,然后处理效率高。 因为两个线程的时候充分利用了CPU等待磁盘IO的空闲时间。曾几何时想过使用多线程读取磁盘数据, 但是读取磁盘数据的性能瓶颈是IO,而不是CPU。 使用多线程的目的是为了不让CPU闲下来,明显不适合用于读取磁盘数据。