【Java】创建多线程的四种方式

一、方式1:继承Thread类

步骤:

  1. 创建一个继承于Thread类的子类
  2. 重写Thread类的run()方法 ----> 此线程执行的操作声明在方法体中
  3. 创建当前Thread子类的对象
  4. 通过实例对象调用start()方法,启动线程 ----> Java虚拟机会调用run()方法

注意main()方法是主线程

1. 创建线程:

//自定义线程类
public class MyThread extends Thread {

	// 共享数据要放在run()方法外边才能被共享且声明为static,否则就是每个线程都会调用run()方法,都会单独拥有一个run()方法里的独享数据,而非共享数据
	//eg: static int trick = 100;
    //定义指定线程名称的构造方法
    public MyThread(String name) {
        //调用父类的String参数的构造方法,指定线程的名称
        super(name);
    }
    /**
     * 重写run方法,完成该线程执行的逻辑
     */
    @Override
    public void run() {
        // 执行的操作
    }
}

2. 调用线程:

public class TestMyThread {
    public static void main(String[] args) {
        //创建自定义线程对象1
        MyThread mt1 = new MyThread("子线程1");
        //开启子线程1
        mt1.start();
        
        //创建自定义线程对象2,分别创建对象开启线程,不可以数据共享,若要共享需要创建static变量
        MyThread mt2 = new MyThread("子线程2");
        //开启子线程2
        mt2.start();
    }
}

3. 创建Thread类的匿名子类的匿名对象

// 创建Thread类的匿名子类的匿名对象,并启动线程
new Thread(){
	public void run(){
		// 执行的操作
	}
}.strat(); // 启动线程

二、方式2:实现Runnable接口

  1. 创建一个实现Runnable接口的类
  2. 实现接口中的run()方法 ----> 线程执行的操作声明在此方法中
  3. 创建实例对象
  4. 将此对象作为参数传到Thread类的构造器中,创建Thread类的实例
  5. 通过Thread的实例对象调用strat()方法,启动线程 ----> Java虚拟机会调用run()方法

最终还是通过Thread实现的

1. 创建线程:

public class MyRunnable implements Runnable {

	// 共享数据要放在run()方法外边才能被共享,否则就是每个线程都会调用run()方法,都会单独拥有一个run()方法里的独享数据,而非共享数据
	//eg: int trick = 100;
    @Override
    public void run() {
        // 执行的操作
    }
}

2. 调用线程

public class TestMyRunnable {
    public static void main(String[] args) {
        //创建自定义类对象  线程任务对象
        MyRunnable mr = new MyRunnable();
        //创建线程对象1
        Thread t1 = new Thread(mr, "长江1");
        t1.start();

		//创建线程对象2,注意两个线程传入的是同一个对象,可以实现数据共享
        Thread t2 = new Thread(mr, "长江2");
        t2.start();
    }
}

3. 创建Runnable接口的匿名子类的匿名对象

new Thread(new Runnable(){
	public run(){
		// 执行的操作
	}
}).start(); // 开启线程

三、继承Thread类 VS 实现Runnable接口

1. 共同点:

  • 都是通过Thread类中定义的start()来启动线程。
  • 都是通过Thread类或其子类的实例对象来创建线程。

2. 不同点:

  • Thread是继承
  • Runnable是实现

3. Runnable好处:

  • 通过实现的方式,避免了类的单继承的局限性
  • 自动共享数据,更适合处理有共享数据的业务逻辑
  • 实现了逻辑代码(在run()方法中)和数据(在创建线程的方法中)的分离

四、Thread类常用方法和生命周期

1 构造器

  • public Thread() :分配一个新的线程对象。
  • public Thread(String name) :分配一个指定名字的新的线程对象。
  • public Thread(Runnable target) :指定创建线程的目标对象,它实现了Runnable接口中的run方法
  • public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。

2 常用方法系列1

  • public void run() :此线程要执行的任务在此处定义代码。
  • public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。
  • public String getName() :获取当前线程名称。
  • public void setName(String name):设置该线程名称。
  • public static Thread currentThread() :返回对当前正在执行的线程对象的引用。在Thread子类中就是this,通常用于主线程和Runnable实现类
  • public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
  • public static void yield():yield只是让当前线程暂停一下,让系统的线程调度器重新调度一次,希望优先级与当前线程相同或更高的其他线程能够获得执行机会,但是这个不能保证,完全有可能的情况是,当某个线程调用了yield方法暂停之后,线程调度器又将其调度出来重新执行。

3 常用方法系列2

  • public final boolean isAlive():测试线程是否处于活动状态。如果线程已经启动且尚未终止,则为活动状态。

  • void join() :等待该线程终止。

    void join(long millis) :等待该线程终止的时间最长为 millis 毫秒。如果millis时间到,将不再等待。

    void join(long millis, int nanos) :等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。

  • public final void stop():已过时,不建议使用。强行结束一个线程的执行,直接进入死亡状态。run()即刻停止,可能会导致一些清理性的工作得不到完成,如文件,数据库等的关闭。同时,会立即释放该线程所持有的所有的锁,导致数据得不到同步的处理,出现数据不一致的问题。

  • void suspend() / void resume() : 这两个操作就好比播放器的暂停和恢复。二者必须成对出现,否则非常容易发生死锁。suspend()调用会导致线程暂停,但不会释放任何锁资源,导致其它线程都无法访问被它占用的锁,直到调用resume()。已过时,不建议使用。

4 常用方法系列3

每个线程都有一定的优先级,同优先级线程组成先进先出队列(先到先服务),使用分时调度策略。优先级高的线程采用抢占式策略,获得较多的执行机会。每个线程默认的优先级都与创建它的父线程具有相同的优先级。

  • Thread类的三个优先级常量:
    • MAX_PRIORITY(10):最高优先级
    • MIN _PRIORITY (1):最低优先级
    • NORM_PRIORITY (5):普通优先级,默认情况下main线程具有普通优先级。
  • public final int getPriority() :返回线程优先级
  • public final void setPriority(int newPriority) :改变线程的优先级,范围在[1,10]之间。

5. 线程的生命周期

jdk1.5 之前的5种状态:
在这里插入图片描述
jdk1.5 之后的6种状态:

  • 将准备和运行合并为一个Runnable状态
  • 将阻塞细分为:计时等待、锁阻塞、无线等待3种状态
    在这里插入图片描述

五、解决线程的安全问题

当多个线程同时操作同一个共享数据时,就会出现线程的安全问题。

方式一:synchronized 同步代码块

/**
*	1.同步监视器,俗称锁。哪个线程获得了锁,哪个线程就能执行该代码块
*	2.同步监视器可以是任何一个类的对象。但是,多个线程必须共用同一个同步监视器,且该对象是唯一的(只有一个实例对象)。
*		>可以自定义一个专用于线程锁的类,然后在这里创建对象作为锁使用。
*		>可以使用this充当锁,this指向当前对象。但是要注意当前类是否只创建了一个对象(保证唯一性)
*			>在继承Thread的方式中不可以使用this,因为每一个线程都需要创建对象,不满足唯一性
*			>在继承Thread的方式中,创建对象作为锁也需要声明为static的才可以,static Object obj = new Pbject();
*			>可以使用“当前类.class”充当锁。因为“Class clz = 类.class”是一个类,仅加载一次,是全局唯一的(反射),该方式在实现Runnable中也可以使用
*	
*/
synchronized(同步监视器){
	// 需要被同步的代码,即操作共享数据的代码
}

方式二:synchronized 同步方法

/*
*用synchronized 直接修饰操作同步代码的方法
*	1.在非static的方法中,默认锁是:this。锁无法修改,因而在继承Thread方法中不适用,可以改成同步代码块方式,手动添加将当前类做为锁
*	2.在static的方法中,默认的锁是:当前类.class。即当前类本身。
*	
*/
// 示例代码:在run()方法中调用show()方法
public synchronized void show(){
	// 操作共享数据的代码
}

方式三(推荐):LOCK

Lock是 java.util.concurrent.locks(JUC)包下的一个接口

ReentrantLock可重复锁:

lock()和unlock()方法直接的代码,就会被加锁:

  • lock:加锁
  • unlock:释放锁

class A{
    //1. 创建Lock的实例,必须确保多个线程共享同一个Lock实例,所以必须是static的,可以在加上final不让后续线程修改
	private static final ReentrantLock lock = new ReenTrantLock();
	public void m(){
        //2. 调动lock(),实现需共享的代码的锁定
		lock.lock();
		try{
			//保证线程安全的代码
		}finally{
            //3. 调用unlock(),释放共享代码的锁定。如果同步代码有异常,要将unlock()写入finally语句块,因为需要保证一定能解锁。
			lock.unlock();  
		}
	}
}

synchronized同步 VS Lock

synchronized:不管是同步方法还是同步代码块,都需要在一对{}之后释放锁。

Lock:

  • 通过lock()和unlock()两个方法对代码加锁,更灵活。
  • Lock作为接口,提供了多种实现类,适合更多更复杂的场景(读锁、写锁、读写锁…)。
  • 效率也更高一些。

wait() VS sleep()

相同点: 一旦 执行,当前线程都会进入阻塞状态。

不同点:

  • 声明位置不同:
    • wait():声明在Object类中
    • sleep():生命在Thread类中
  • 使用场景不同
    • wait():只能使用在synchronized同步代码块或同步方法中
    • sleep():在一在任何场景使用
  • 对锁的控制
    • wait():一旦执行,会释放锁
    • sleep():一旦执行,不会释放锁
  • 结束阻塞的方式:
    • wait():到达时间自动结束阻塞,或被notify()唤醒结束阻塞
    • sleep():到达时间自动结束阻塞

六、死锁

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。

1. 诱发死锁的原因:

  • 互斥条件:一个线程拿到了锁,在释放之前其他线程无法得到锁
  • 占用且等待:拿着一个锁还在等待其他锁,导致无法继续执行
  • 不可抢夺(或不可抢占):拿着一个锁还在等待其他锁的同时,其他线程先获取到了另一个锁,但是不能先执行
  • 循环等待:一致保持不可抢夺的状态

2. 解决死锁方案

死锁一旦出现,基本很难人为干预,只能尽量规避。可以考虑打破上面的诱发条件。

  • 针对条件1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。
  • 针对条件2:可以考虑一次性申请所有所需的资源(获取所有需要用到的锁,用完后立即释放),这样就不存在等待的问题。
  • 针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到(在一定时间内),就主动释放掉已经占用的资源。
  • 针对条件4:可以将资源改为线性顺序(优先级)。申请资源时,先申请序号较小的,这样避免循环等待问题。

七、方式3:实现Callable接口

  • 与使用Runnable相比, Callable功能更强大些

    • 相比run()方法,call()可以有返回值,更灵活
    • call()方法可以使用throws抛出异常,更灵活
    • Callable使用泛型参数,可以指明具体的call()返回值类型,更灵活
  • Future接口(了解)

    • 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
    • FutureTask是Futrue接口的唯一的实现类
    • FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
  • 缺点:在获取分线程执行结果的时候,当前线程(或是主线程)受阻塞,效率较低。

代码举例:

/*
 * 创建多线程的方式三:实现Callable (jdk5.0新增的)
 */
//1.创建一个实现Callable的实现类
class NumThread implements Callable {
    //2.实现call方法,将此线程需要执行的操作声明在call()中
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}


public class CallableTest {
    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();


//      接收返回值
        try {
            //6.获取Callable中call方法的返回值
            //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
            Object sum = futureTask.get();
            System.out.println("总和为:" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

}

八、方式4(推荐):使用线程池

背景
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

那么有没有一种办法使得线程可以复用,即执行完一个任务,并不被销毁,而是可以继续执行其他的任务?

思路: 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。
在这里插入图片描述
好处:

  • 提高了程序执行的效率(线程已经提前创建好了)
  • 提高了资源复用率(执行完的线程并未销毁,还可以继续执行其他任务,不需要每次都创建)
  • 可以设置相关参数,对线程池中的线程进行管理
    • corePoolSize:核心池的大小
    • maximumPoolSize:最大线程数
    • keepAliveTime:线程没有任务时最多保持多长时间后会终止

线程池相关API:

  • JDK5.0之前,我们必须手动自定义线程池。从JDK5.0开始,Java内置线程池相关的API。在java.util.concurrent包下提供了线程池相关API:ExecutorService 和 Executors。
  • ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
    • void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
    • Future submit(Callable task):执行任务,有返回值,一般又来执行Callable
    • void shutdown() :关闭连接池
  • Executors:一个线程池的工厂类,通过此类的静态工厂方法可以创建多种类型的线程池对象。
    • Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
    • Executors.newFixedThreadPool(int nThreads); 创建一个可重用固定线程数的线程池
    • Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
    • Executors.newScheduledThreadPool(int corePoolSize):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
// 这里使用实现Runnable的方式
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);
            }
        }
    }
}

class NumberThread1 implements Runnable{

    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 != 0){
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

class NumberThread2 implements Callable {
    @Override
    public Object call() throws Exception {
        int evenSum = 0;//记录偶数的和
        for(int i = 0;i <= 100;i++){
            if(i % 2 == 0){
                evenSum += i;
            }
        }
        return evenSum;
    }

}

public class ThreadPoolTest {

    public static void main(String[] args) {
        //1. 提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//        //设置线程池的属性
//        System.out.println(service.getClass());//ThreadPoolExecutor
        service1.setMaximumPoolSize(50); //设置线程池中线程数的上限

        //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());//适合适用于Runnable
        service.execute(new NumberThread1());//适合适用于Runnable

        try {
            Future future = service.submit(new NumberThread2());//适合使用于Callable
            System.out.println("总和为:" + future.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
        //3.关闭连接池
        service.shutdown();
    }

}

九、线程调度策略

分时调度: 所有线程轮流使用CPU的使用权,并且平均分配每个线程占用CPU的时间。

抢占式调度: 优先级高的线程使用CPU的概率大些。如果线程优先级相同,则会随机选择一个(线程随机性),Java使用的是抢占式调度。

  • 0
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CODER-V

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值