多线程之线程创建、加锁同步、通信(Java学习笔记六)

多线程

多线程的优点:

  • 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
  • 提高计算机系统CPU的利用率。
  • 改善程序结构,将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
程序、进程、线程

程序:为完成特定任务,用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。

进程:是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程,有它自身的产生、存在和消亡的过程(生命周期)。进程是资源分配的最小单位,系统在运行时会为每个进程分配不同的内存区域。

线程:进程进一步细化为线程,是程序内部的一条执行路径。

  • 若一个进程同一时间并行执行多个线程,即为支持多线程。
  • 线程是调度和执行的最小单位,每个线程拥有独立的运行栈和程序计数器,线程切换的开销小(进程拥有独立的堆和方法区)。
  • 一个进程中的多个线程共享相同的内存单元/内存地址空间,从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源会带来安全隐患,
并行和并发

并行:多个CPU同时执行多个任务,比如多个人同时做不同事情。

并发:一个CPU采用时间片同时执行多个任务,比如一个人做多件事。(宏观上并行,微观上串行)

同步与异步

传送门:(讲的不错)

https://blog.csdn.net/hasijingsi/article/details/105285426

线程常用方法
  • void start():启动线程,并执行对象的run()方法。
  • run():线程在被调度时执行的操作。通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中。
  • String getName():返回线程的名称。
  • void setName(“xxx”):设置该线程的名称(或者根据构造函数命名)。
  • static Thread currentThread():返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类。
  • yield():释放当前CPU的使用权(可能释放后又得到/抢到了使用权)。
  • join():在线程A调用线程B的join()方法,此时线程A进入阻塞状态,直到线程B执行完毕。
  • stop():强制线程生命期结束(不建议使用)。
  • sleep(long millitime):阻塞线程millitime毫秒(睡眠)。
  • isAlive():判断线程是否存活。
线程优先级

同优先级线程组成先进先出队列(先到先服务),使用时间片策略。

对高优先级,使用优先调度的抢占式策略。

优先等级:

public static final int MIN_PRIORITY = 1;
/**
  * The default priority that is assigned to a thread.
  */
public static final int NORM_PRIORITY = 5;
/**
  * The maximum priority that a thread can have.
  */
public static final int MAX_PRIORITY = 10;

涉及到的方法:

getPriority():获取当前线程优先级;

setPriority(int n):设置当前线程优先级;

高优先级线程要抢占低优先级线程CPU的执行权,但是只能从概率上讲,高优先级线程高概率下被执行,但不意味着必须高优先级线程执行完后低优先级线程才会执行。

线程分类

守护线程、用户线程

二者唯一区别是判断JVM何时离开。

守护线程是用来服务用户线程的,通过在start()方法前调用thread.setDaemon(true)可以把一个用户线程变成一个守护线程。

Java垃圾回收就是一个典型的守护线程。

如果JVM中都是守护线程,则当前JVM将退出。

线程生命周期

JDK中Thread.State类定义了以下几种线程状态:

新建、就绪、运行、阻塞、死亡。

public enum State { 
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

线程安全

多个线程执行的不确定性引起执行结果的不稳定;多个线程对数据的共享,会造成操作的不完整性,会破坏数据,即线程安全问题。

解决:当一个线程在操作共享数据时,其他线程不能参与进来,直到这个线程操作完毕,其他线程才可以参与进来开始操作数据。(这种情况即使该线程出现阻塞,也不能改变)

死锁

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。

出现死锁后,不会出现异常,不会出现提示,只是所有线程都处于阻塞状态,无法继续。使用同步时,要避免出现死锁。

synchronized

Java中,通过同步机制来解决线程的安全问题。

方法一:同步代码块(synchronized)

synchronized (同步监视器){
    //需要被同步的代码(操作共享数据的代码)
}

同步监视器:俗称锁。任何一个类的对象都可以充当锁。(要求多个线程必须共用同一把锁)

实现Runnable接口方式中同步监视器常用this,继承Thread类方式中同步监视器常用(类名.class)。(因为this不唯一)

好处:解决了线程安全问题。坏处:效率低。

方法二:同步方法

如果操作共享数据的代码完整的声明在一个方法中,可以将此方法声明为同步的。同步监视器默认为this。

class MyThread2 implements Runnable{
    @Override
    public void run() {
    	method();
        //相关操作
    }
    
    private synchronized void method (同步监视器){
    //需要被同步的代码(操作共享数据的代码)
	}
}


如果是继承Thread类的方式,则需要将方法声明为static。

同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。

非静态的同步方法,同步监视器是this。

静态的同步方法,同步监视器是当前类本身。

利用此方式即可实现线程安全的单例模式。

Lock(锁)

JDK5.O开始,Java通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class MyThread2 implements Runnable{
    private ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        try{
            //加锁
            lock.lock();
            //相关操作
			...   
        }finally {
            //释放锁
            lock.unlock();
        }
    }
}

Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应该先获得Lock锁对象。

ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁,释放锁。

synchronized和lock的异同?

同:二者都可以解决线程安全问题。

异:synchronized机制在执行完相应的同步代码后可以自动释放锁,lock则需要手动启动同步(加锁lock()),同时结束同步(释放锁unlock())也需要手动实现。

线程通信

线程通信的三个方法,定义在java.lang.Object类中:

  • wait():让当前线程进入阻塞状态,同时,wait()也会让当前线程释放它所持有的锁。直到其他线程调用notify()或notifyAll(),当前线程才能被唤醒(进入“就绪状态”)。
  • notify() :唤醒优先级高的那个进程。
  • notifyAll():唤醒全部进程。

三个方法必须使用在同步代码块或同步方法中。

三个方法的调用者必须是同步代码块或同步方法中的同步监视器(默认this,通常省略)。

sleep()和wait()的异同?

相同点:二者都可以使得当前线程进入阻塞状态。

不同点:

  1. 声明位置不同:Thread类中声明sleep(),Object类声明wait()。
  2. 调用的要求不同,sleep()可以在任何需要的场景下调用,wait()必须在同步代码块或同步方法中。
  3. 如果二者都使用在同步代码块或同步方法中,sleep()不会释放锁,但wait()会释放锁。

创建线程的四种方法

Thread

1.创建一个继承于Thread类的子类;

2.重写Thread类的run()方法,将此线程执行的操作声明在run()方法中;

3.创建Thread类的子类的对象;

4.通过此对象调用start()方法。

public class Day06 {
    public static void main(String[] args) {

        MyThread myThread = new MyThread("线程一");
        MyThread myThread1 = new MyThread("线程二");
        myThread.start();
        myThread1.start();
        //如下操作仍然是在main线程中执行
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 1) System.out.println(i);
        }
    }

}

class MyThread extends Thread {

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

    }
}

start方法启动当前线程,调用当前线程的run()方法。

不能通过直接调用run()方法的方式启动线程。

继承方式(创建Thread类的匿名子类的方式):

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

1.创建一个实现了Runnable接口的类;

2.实现类去实现Runnable中的抽象方法;

3.创建实现类的对象;

4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象;

5.通过Thread类的对象调用start()方法;

MyThread2 myThread2 = new MyThread2();
Thread myThread3 = new Thread(myThread2);
myThread3.start();

class MyThread2 implements Runnable{
    @Override
    public void run() {
        //相关操作
    }
}

两种创建线程方式的比较:

开发中,优先选择实现Runnable接口的方式。

原因:实现方式没有类的单继承性的局限性;实现方式更适合来处理多个线程有共享数据的情况;

二者联系:Thread类本身也实现了Runnable接口;两种方式都需要重写run()方法,将线程要执行的逻辑声明在run()中。

Callable接口

JDK5.0后新增Callable接口来创建线程。和Runnable相比,Callable功能更强大:

  • 相比run()方法,可以有返回值;
  • 方法可以抛出异常;
  • 支持泛型的返回值;
  • 需要借助FutureTask类,比如获取返回结果。

FutureTask类来自Future接口:

  • 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
  • FutrueTask是Futrue接口的唯一的实现类
  • FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
public class Day06 {
public static void main(String[] args) {
    //3.新建Callable实现类的对象
    MyThread3 thread3 = new MyThread3();
    //4.将此对象作为参数传入FutureTask的构造器中,创造FutureTask对象。
    FutureTask<Object> objectFutureTask = new FutureTask<Object>(thread3);
    //5.将创建的 FutureTask对象作为参数传入Thread类中,创建Thread对象,并调用其start()方法
    new Thread(objectFutureTask).start();
    try {
        //获取重写后Call()方法中的返回值
        //get方法的返回值即为FutureTask构造器参数Callable实现类重写的call方法的返回值
        Object o = objectFutureTask.get();
        System.out.println("总和为"+o);
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }
}
//1.创建实现类实现Callable接口
class MyThread3 implements Callable<Object> {
    @Override
    public Object call() throws Exception {
        //2.将操作写入call()方法
        int sum = 0;
        for (int i = 0; i < 100; i++) {
            sum += i;
        }

        return sum;
    }
}
线程池

背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。

思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交 通工具。

好处:

  • 提高响应速度(减少了创建新线程的时间)
  • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  • 便于线程管理
    • corePoolSize:核心池的大小 ;
    • maximumPoolSize:最大线程数 ;
    • keepAliveTime:线程没有任务时最多保持多长时间后会终止 ;
    • ……

JDK 5.0起提供了线程池相关API:ExecutorService和Executors

  • ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
    • void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行 Runnable
    • Future submit(Callable task):执行任务,有返回值,一般又来执行 Callable
    • void shutdown() :关闭连接池
  • Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
    • Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
    • Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
    • Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
    • Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运 行命令或者定期地执行。
class NumberThread implements Runnable{

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

public class ThreadPool {

    public static void main(String[] args) {
        //1.提供指定线程数的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        
        //设置线程池属性
        // ThreadPoolExecutor service = (ThreadPoolExecutor) executorService;
        // service.setCorePoolSize(15);
        // service.setMaximumPoolSize(15);
        
        //2.执行指定的线程操作,需要提供实现Runnable接口或Callable接口的对象
        executorService.execute(new NumberThread());//适合Runnable
        //executorService.submit();//适合Callable
        //3.关闭线程池
        executorService.shutdown();
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值