多线程
- 多线程程序的优点:
- 提高应用程序的响应,对图形化界面更有意义,可增加用户体验。
- 提高计算机系统CPU的利用率。
- 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。
- 何时需要多线程。
- 同时执行两个或多个任务。
- 需要实现一些需要等待的任务时。
- 需要一些后台运行的程序时。
- 线程的创建和启动
- Java语言的JVM允许程序运行多个线程,它通过Java.lang.Thread类来体现。
多线程的创建,方式一:继承于Thread类
- 创建一个继承于Thread类的子类。
- 重写Thread类的run()方法 ------> 将此线程执行的操作声明在run()中
- 创建Thread类的子类的对象。
- 通过此对象调用start()。
eg: 遍历100以内所有的偶数。
// 1. 创建一个继承于Thread类的子类
class MyThread extends Thread{
//2. 重写Thread类的run()
@Override
public void run(){
for(int i = 0; i < 100; i++){
if(i % 2 == 0){
System.out.println(i);
}
}
}
}
//下面是主线程,对象在主线程中被造出,调用t1时是分线程开始执行。
public class ThreadTest{
public static void main(String[] args){
//3. 创建Thread类的子类的对象
MyThread t1 = new MyThread(); // 小括号内会出现一个 introduce local variable
//4.通过此对象调用start()方法。 (start是Thread中定义的方法)
t1.start();
for(int i = 0; i < 100; i++){
if(i % 2 != 0){
System.out.println(i);
}
}
}
start()方法作用?
①启动当前线程。
②调用当前线程的run()方法。 如果直接调用run()方法,就相当于没有分线程,只有一个主线程是在调用其它类的方法。
练习
package com.atguigu.exer;
/**
* 练习:创造两个分线程,其中一个线程遍历100以内的偶数,另一线程遍历100以内的奇数。
*/
public class ThreadDemo {
public static void main(String[] args) {
MyThread1 m1 = new MyThread1();
MyThread2 m2 = new MyThread2();
m1.start();
m2.start();
//或者创建Thread类的匿名子类的方式
/** new Thread(){
@Override
public void run() {
for(int i = 0; i < 100; i++){
if(i % 2 == 0){ System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}.start();
}
*/
}
}
class MyThread1 extends Thread{
@Override
public void run() {
for(int i = 0; i < 100; i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
class MyThread2 extends Thread{
@Override
public void run() {
for(int i = 0; i < 100; i++){
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
-
i.sout = System.out.prinln(“i”);
-
main = public static void main (String[] args){ }
-
Thread.currentThread(),getName() + ":" + i
打出的为当前线路的号 eg:Thread-0:10 Thread-1:11
Thread类的有关方法
-
void start( ) : 启动线程,并执行对象的run()方法。
-
run( ) : 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中。线程在被调度时执行的操作。
-
String getName( ) : 获取当前线程的名称。
-
void setName(String name) : 设置当前线程的名称。
-
static Thread currentThread( ) : 静态方法,返回执行当前的线程。在Thread子类中就是this,通常用于主线程和Runnable实现类。
-
yield( ) : 释放当前cpu的执行权。
eg: if(i % 2 == 0){ yield( ); }满足这个条件调入yield,释放此线程,可能会被其他线程替换下来。
-
join( ) : 在 线程a中 调用 线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完之后,线程a才结束阻塞状态。
eg:在执行join时此线程放开,执行其他线程,并且将其他线程全部执行完后才可能继续执行此线程。
-
**slepp (long millitime) ** : 让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程为阻塞状态。 参数里面填时间(毫秒为单位)。
eg:
try { sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
-
stop( ) :(已经过时了) 强制线程生命期结束。
-
boolean isAlive( ) : 返回boolean,判断线程是否还活着。
eg:
System.out.println(h1.isAlive());
线程的调度
- 线程的优先级等级: 最高 10、最低 1、 默认优先级: 5。
MAX_PRIORITY: 10
MIN_PRIORITY: 1
NORM_PRIORITY: 5
-
涉及到方法
getPriority( ) :返回线程优先值。
setPriority(int newPriority) : 改变线程的优先级。
eg:
System.out.println( Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() );
//设置h1的优先级 h1.setPriority(Thread.MAX_PRIORITY);
-
说明:
- 线程创建时继承父线程的优先级。
- 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用。
相关习题
- 三个窗口买票问题,总票数应设定为static,使得三个分线程的对象都使用同一个变量。
多线程的创建,方式二: 实现Runnable接口
- 创建一个实现了Runnable接口的类。
- 实现类去实现Runnable中的抽象方法: run( )。
- 创建实现类的对象
- 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象。
- 通过Thread类的对象调用start( ).
eg:
package com.atguigu.java.Thread.Method;
// 1. 创建了一个实现了Runnable接口的类
class MThread implements Runnable{
//2.实现类去实现Runnable中的抽象方法:run()
@Override
public void run() {
for(int i = 0; i < 100; i++){
if(i % 2 == 0){
System.out.println(i);
}
}
}
}
public class ThreadTest1 {
public static void main(String[] args) {
//3. 创建实现类的对象
//也可以 new MThread(); + alt + enter
MThread mThread = new MThread();
//4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象。
Thread t1 = new Thread(mThread);//父类调用子类对象 (多态的一种形式)
t1.setName("线程1");
//5. 通过Thread类的对象调用start()----->调用当前线程的tun()------> 调用了Runnable类型的target的tun()
t1.start();
Thread t2 = new Thread();
t2.setName("线程2");
t2.start();
}
}
- 买票问题,在此类创建方法下总票数就不需要加static,因为 类的对象只new了一个,所以面对的是同一个对象,用的也是同一个总票数。
比较创建线程的两种方式
开发中,优先选择:实现Runnable接口的方式。
-
原因: 1. 实现的方式没有类的单继承性的局限性
- 实现的方式更适合来处理多个线程有共享数据的情况。
-
联系: public class Thread implements Runnable (也就是说,Thread类也接口了Runnable)
-
相同点:两种方式都需要重写run( ), 将线程要执行的逻辑声明在run()中。
目前两种方式,想要启动线程,都是调用Thread类中的start( )。
-
线程通信: wait( ) / notify( ) / notifyAll( ) 此三个方法定义在Object类中。
线程的生命周期
- 新建 :当一个Tread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。
- 就绪 :处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行条件,只是没分配到CPU资源。
- 运行 :当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能。
- 阻塞 :在某种特殊情况下,被人为挂起或执行输出输入操作时,让出CPU并临时中止自己的执行,进入阻塞状态。
- 死亡 : 线程完成了它的全部工作或线程被提前强制性的中止或出现异常导致结束。
线程的同步
eg:创建三个窗口卖票,总票数为100张,使用实现Runnable接口的方式。
-
问题: 卖票的过程中,出现了重票、错票(0, -1等)------->出现了线程的安全问题。
-
问题出现的原因:
-
如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来,直到线程a操作完ticket时,线程才开始操作ticket。这种情况即使线程a出现了阻塞,线程也不会被改变。
-
在java中:我们通过同步机制,来解决线程的安全问题。
-
方式一:同步代码块
synchronized(同步监视器){ //需要被同步的代码 }
说明:1. 操作共享数据的代码,即为需要被同步的代码。----> 不能包含代码多了,也不能包含代码少了。
-
共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
-
同步监视器,俗称 ” 锁 “ 。任何一个类的对象,都可以充当锁。
要求:多个线程必须要共用同一把锁。
-
- 方式二:如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。
-
-
同步的方式,解决了线程的安全问题。----好处
操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。
- 接口方式的
-
- 继承方式的
在继承Thread类创建多线程方式中,慎用this充当同步监视器,考虑使用当前类充当同步显示其。
- 继承方式的
- 同步的方法二:
使用同步方法解决实现Runnable接口的线程安全问题
public synchronized void run(){
}
关于同步方法的总结:
-
同步方法仍然涉及到同步监视器,只是不需要我们显示的声明。
-
非静态的同步方法,同步监视器是:this.
静态的同步方法,同步监视器是:当前类本身。
线程的死锁问题
死锁: 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
解决方法:
- 专门的算法、原则。
- 尽量减少同步资源的定义。
- 尽量避免嵌套同步。
解决线程安全问题的方式三:Lock锁---- JDK5.0新增
- 实例化ReentrantLock
private ReentranLock lock = new ReentrantLock();
-
调用lock方法
while(true){ try{ //调用锁定lock方法 try finally lock.lock(); if(ticket > 0){ try{ Thread.sleep(nillis:100); }catch(InterruptedException e){ e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":"); ticket--; }else{ break; } }finally{ //3. 调用解锁的方法:unlock lock.unlock(); } }
synchronized 与lock 的不同:
-
synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
-
lock需要手动的启动同步( lock() ), 同时结束同步也需要手动(unlock())。
线程的通信
while(true){
synchronized(this){
//唤醒线程(单个),如果是多个的话用notifyAll()
notify();
if(number <= 100){
system.out.println(Thread.currentThread().getName() + ":" + number);
number++;
//使得调用如下wait()方法的线程进入阻塞状态。
wait();
}
}
}
涉及到的三个方法:
- wait() : 一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
- notify() : 一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程被wait,就唤醒优先级高的那个。
- notifyAll() : 一旦执行此方法,就会唤醒所有被wait的线程。
说明:
-
以上三个方法必须使用在同步代码块或同步方法中。
-
以上三个方法的调用者必须是同步代码块或同步方法中的同步监视器。
否则会出现 Il legalMonitorStateException异常。
面试题: slepp( ) 与 wait()的异同
-
相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
-
-
两个方法声明的位置不同:
- Thread类中声明sleep()
- Object类中声明wait()。
-
调用的要求不同:
- sleep()可以在任何需要的场景下调用。
- wait()必须要使用在同步代码块或者同步方法中。
-
关于是否释放同步监视器:如果两个方法都是用在同步代码块或同步方法中,
- slepp()不会释放同步监视器(锁)。
- wait()会释放同步监视器(锁)。
-
线程的创建方式三: callable接口
- 相比run()方法,可以有返回值。
- 方法可以抛出异常。
- 支持泛型的返回值。
- 需要借助FutureTask类,比如获取返回结果。
- Future接口
- 可以对具体的Runnable、Callable任务的执行结果进行取消、查询是否完成,获取结果等。
- FutrueTask是Futrue接口的唯一的实现类。
- FutureTask同时实现了Runnable,Future接口,它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。
- Future接口
//1. 创建一个实现Callable的实现类
class NumThread implements Callable{
//2. 实现call方法,将此线程需要执行的操作声明在call()中,同时call()中可以有返回值。
public Object call() throws Exception{
for(int i = 1; i < 100; i++){
if(i % 2 == 0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class ThreadNew{
public static void main(String[] args){
//3. 创建Callable接口实现类的对象
NumThread numThread = new NumThread();
//4. 将Callable接口实现类的对象作为参数传递到FutureTask();构造器中。创建FutureTask的对象。
FutureTask futureTask = new FutureTask(NumThread);
//5. 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()。
new Thread(futureTask).start();
//get()返回值即为FutureTask构造器参数Callable实现类重写的cal()返回值
//6. 获取Callable中的call()中的返回值。 (可要可不要)
Object sum = futureTask.get();
System.out.println(sum);
}
}
- 如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
- call()可以有返回值的。
- call()可以抛出异常。
- Callable是支持泛型的。
线程的创建方式四:使用线程池
- 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中,可避免频繁创建销毁、实现重复利用。
- 好处:
- 提高响应速度。(减少了创建新线程的时间)
- 降低资源消耗。(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理。
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用了线程池中线程,不需要每次都创建)
- 便于线程管理
- corePoolSize : 核心池的大小。
- maximumPoolSize : 最大线程数。
- keepAliveTime : 线程没有任务时最多保持多长时间后会终止。
class NumberThread implements Runnable{
@Override
public void run() {
for(int i = 0; i <= 100; i++)
{
if(i % 2 == 0)
{
System.out.println(Thread.currentThread().getName() + " : " + i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//造了一个线程池,里面有十个线程。
//1. 提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service; //强制转换类型
//设置线程池的属性
System.out.println(service.getClass()); //ThreadPoolExecutor 获取实现类
service1.setCorePoolSize(15);
//2. 执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread()); //适合使用于Runnable
//service.submit(); 适合适用于Callable
//3. 关闭连接池
service.shutdown();
}
}