多线程

线程

线程(Thread)是一个程序内部的一条执行流程

程序中如果只有一条执行流程,那这个程序就是单线程的程序

多线程是指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度)

如何创建多条线程:Java是通过java.lang.Thread类的对象来代表线程的

多线程的创建方式一:继承Thread类

步骤

1.定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法

2.创建MyThread类的对象

3.调用线程对象的strat()方法启动线程(启动后还是执行run方法)

public class ThreadTest1{
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();

        for (int i = 1; i <= 5; i++) {
            System.out.println("主线程的方法执行了" + i);
        }

    }
}

优缺点

优点:编码简单

缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展

注意事项

1.启动线程必须是调用strat方法,不是调用run方法(调用run方法就相当于调用了一个普通的方法,而不会创建一条新的线程)

2.main方法是由一条默认的主线程负责执行,调用的线程称为子线程,主线程和子线程可以同时运行

3.不要把主线程任务放在启动子线程之前,否则在主线程任务执行完毕后才会启动子线程,相当于没有形成多线程

多线程创建方式二:实现Runnable接口

步骤

1.定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法

2.创建MyRunnable任务对象(可以用多态的方式,Thread类实现了Runnable接口)

3.把MyRunnable任务对象交给Thread处理

Thread类提供的构造器

说明

public Thread(Runnable target)

封装Runnable对象成为线程对象

4.调用线程对象的start()方法启动线程

public class ThreadTest2 {
    public static void main(String[] args) {
        MyRunnable target = new MyRunnable();

        new Thread(target).start();

        for (int i = 1; i <= 5; i++) {
            System.out.println("主线程启动了" + i);
        }
    }
}

匿名内部类写法

public class ThreadTest2 {
    public static void main(String[] args) {
        //匿名内部类写法
       Runnable target = new Thread(new Runnable() {
           @Override
           public void run() {
               for (int i = 1; i <= 5; i++) {
                   System.out.println("子线程启动了" + i);
               }
           }
       });
        new Thread(target).start();

        //简化形式
        new Thread(new Runnable() {
           @Override
           public void run() {
               for (int i = 1; i <= 5; i++) {
                   System.out.println("子线程启动了" + i);
               }
           }
       }).start;
        
        for (int i = 1; i <= 5; i++) {
            System.out.println("主线程启动了" + i);
        }
    }
}

该匿名内部类的形式除了以上的简化形式外还可以用Lambda表达式简化

优缺点

优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强

缺点:需要多一个Runnable对象

多线程的创建方式三:利用Callable接口、FutureTask类来实现

步骤

1.创建任务对象

定义一个类实现Callable接口,重写call方法,封装要做的事情,和要返回的数据

把Callable类型的对象封装成FutureTask(线程任务对象)

2.把线程任务对象交给Thread对象

3.调用Thread对象的start方法启动线程

4.线程执行完毕后,可以通过FutureTask对象的get方法去获得线程任务执行的结果

注意事项

1.Callable是一个泛型类,可以定义返回的结果的类型

2.使用get方法得到的结果可能是异常,因此要处理异常

3.如果主线程调用get方法,但是子线程还没有执行完毕,主线程会先暂停,等到子线程执行完毕在获得结果

FutureTask的API

构造器

说明

public FutureTask<>(Callable call)

把Callable对象封装成FutureTask对象

方法

说明

public V get() throws Exception

获取线程执行call方法返回的结果

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class ThreadTest3 {
    public static void main(String[] args) throws Exception {
        Callable call = new MyCallable(100);
        FutureTask f = new FutureTask<>(call);
        new Thread(f).start();

        //注:这里接收到的子线程的执行的结果可能是异常
        //如果主线程到这里,子线程还没结束,会先暂停,等到子线程的执行完毕后才会获取结果
        System.out.println(f.get());

    }
}

优缺点

优点:

1.线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。

2.可以在线程执行完毕后去获取线程执行的结果

缺点:编码复杂一点

Thread的常用方法

常用方法

说明

public void run()

线程的任务方法

public void start()

启动线程

public String getName()

获取当前线程的名称,线程名称默认是Thread-索引

public void setName(String Name)

为线程设置名称

public static Thread currentThread()

获取当前执行的线程对象

public static void sleep(long time)

让当前执行的线程休眠多少毫秒后,再继续执行

public final void join()...

暂停主线程,等调用当前这个方法的线程执行完之后在开启主线程(如果之前有多个子线程再执行,然后一个子线程调用该方法,只会暂停主线程,另一个子线程不影响,两个子线程会一起执行)

构造器

说明

public Thread(String name)

可以为当前线程指定名称

public Thread(Runnable targer)

封装Runnable对象成为线程对象

public Thread(Runnable targer,String name)

封装Runnable对象成为线程对象,并指定线程名称

注意事项

我们自己创建的线程都是Thread类的子孙类,当我们用继承了Thread的类创建对象时,如果要在创建对象的时候为线程指定名称,可以在该线程类中加一个有参构造器,并直接调用父类的有参构造器[super(name)]

//直接调用父类的有参构造器
    public MyThread(String name){
        super(name);
    }

线程安全

多个线程,同时访问同一个共享资源,且存在修改该资源

线程同步

认识线程同步

加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来

方式一、同步代码块

作用:把访问共享资源的核心代码上锁,以此包装线程安全

synchronized(同步锁){
    访问共享资源的核心代码
}

原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行

//1.同步代码块上锁
    public void drawMoney(int number) {
        synchronized (this) {
            //1.判断余额是否足够
            if (number > money){
                System.out.println("余额不足");
                return;
            }

            //让线程停一下,确保会出现线程安全问题
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //2.取钱
            System.out.println(name + "成功取钱" + number + "元");

            //3.更新账户
            money -= number;
        }
    }

注意事项:

1.同心锁是一个对象

2.对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug

锁对象的使用规范

建议使用共享资源作为锁对象

1.对于实例方法建议用this作为锁对象

2.对于静态方法建议使用字节码(类名.class)对象作为锁对象

方式二、同步方法

作用:把访问共享资源的核心方法给上锁,依次保证线程安全

格式:

修饰符 synchronized 返回值类型 方法名称(形参列表){

操作共享资源的代码

}

原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行

//2.同步方法上锁
    public synchronized void drawMoney(int number) {
        String name = Thread.currentThread().getName();
            //1.判断余额是否足够
            if (number > money){
                System.out.println("余额不足");
                return;
            }

            //让线程停一下,确保会出现线程安全问题
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //2.取钱
            System.out.println(name + "成功取钱" + number + "元");

            //3.更新账户
            money -= number;
    }

底层原理:

同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码

如果方法是实例方法:同步方法默认用this作为的锁对象

如果方法是静态方法:同步方法默认用类名.class作为的锁对象

同步代码块和同步方法的比较

范围上:同步代码块锁的范围更小,同步方法锁的范围更大

性能上:同步代码块锁的范围小,因此性能会好一点(对于现在的计算机而言可以忽略)

可读性:同步方法更好

方式三、Lock锁

Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁

Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建锁对象

构造器

说明

public ReentrantLock()

获得Lock锁的实现类对象

方法名

说明

void lock()

获得锁

void unlock()

释放锁

//3.Lock锁
    public void drawMoney(int number) {
        String name = Thread.currentThread().getName();

        try {
            //上锁
            lk.lock();
            //1.判断余额是否足够
            if (number > money){
                System.out.println("余额不足");
                return;
            }

            //让线程停一下,确保会出现线程安全问题
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //2.取钱
            System.out.println(name + "成功取钱" + number + "元");

            //3.更新账户
            money -= number;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //解锁
            lk.unlock();
        }
    }

注意事项:

1.锁对象一般是专属的,可以用final修饰

2.如果锁住的代码出了异常,会直接停止运行,可能会导致无法释放锁,因此要使用try-catch-finally来捕捉异常并释放锁

线程通信(了解)

线程通信

当多个线程共同操作共享的资源时,线程通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源竞争

常见模型

生产者线程负责生产数据

消费者线程负责消费生产者生产的数据

注意:生产者生产完数据应该等待自己,通知消费者消费;消费者消费完数据也应该等待自己,并通知生产者生产

Object类的等待和唤醒方法

方法名

说明

void wait()

让当前线程等待并释放所占锁,直到另一个线程调用notify()方法或notifyAll()方法

void notify()

唤醒正在等待的单个线程

void notifyAll()

唤醒正在等待的所有线程

注意:
1.上述方法应该使用当前同步锁对象进行调用,否则会出bug

2.等待应该在唤醒后面,否则等待自己之后就无法执行唤醒操作

线程池

认识线程池

线程池就是一个可以复用线程的技术

不适用线程池的问题

用户每发起一个请求,后台就需要创建一个新线程来处理,且创建新线程的开销很大,当请求过多时,会产生大量线程,严重影响系统性能

线程池的工作原理

线程池里有一块区域来放置固定数量的线程,称之为工作线程(或核心线程);一块区域来放置要处理的任务,称之为任务队列,线程会不断处理任务,完成后再去处理下一个任务;

注:每个任务都是一个对象,这些对象必须实现任务接口(Runnable或Callable)

创建线程池

JDK5.0起提供了代表线程池的接口: ExecutorService

得到线程池对象

使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象

ThreadPoolExecutor构造器

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,

TimeUnit unit, BlockingQueue<Runnable> workQueue,

ThreadFactory threadFactory,RejectedExeutionHandler handler)

参数一:corePoolSize:指定线程池的核心线程的数量

参数二:maximumPoolSize:指定线程池的最大线程数量(除了核心线程,还有临时线程)

参数三:keepAliveTime:指定临时线程存活的时间

参数四:unit:指定临时现场存货的时间单位(秒、分、时、天)(TimeUnit是一个枚举类,直接调用里面的对象即可)

参数五:workQueue:指定线程池的任务队列

参数六:threadFactory:指定线程池的线程工厂

参数七:handler:指定线程池的任务拒绝策略(线程都在忙,任务列表也满了的时候,新任务来了该怎么处理)

注意事项
1.临时线程什么时候创建?

新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程

2.什么时候会开始拒绝新任务?

核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务

注:正在被处理的任务不在任务队列中

3.线程池中核心线程的创建

当线程池接到新任务时,首先线程池会判断当前已创建的线程是否小于 corePoolSize (核心线程数),如果小于,则无论已创建的线程是否空闲,都会选择创建一个新的线程来执行该任务,直到已创建的线程等于核心线程数

4.还可以使用Excutors(线程池的工具类)调用方法返回不同特点的线程池对象,但使用这些方法创建的线程池可能会出现系统风险,同时阿里巴巴手册中的规范也禁止使用该方法,因此开发中只使用ThreadPoolExecutor来创建线程池

线程池处理Runnable、Callable任务

ExecutorService的常用方法

方法名

说明

void execute(Runnable command)

执行Runnable任务

Future<T> submit(Callable<T> task)

执行Callable任务,返回未来任务对象,用于获取线程返回的结果

void shutdown()

等全部任务执行完毕后,在关闭线程池

List<Runnable> shutdownNow()

立即关闭线程池,停止正在执行的任务,并返回队列中未执行的任务

新任务拒绝策略

策略

说明

ThreadPoolExecutor.AbortPolicy

丢弃任务并抛RejectedExecutionException异常,是默认的策略

ThreadPoolExecutor.DiscardPolicy

丢弃任务,但是不抛出异常,是不推荐的做法

ThreadPoolExecutor.DiscardOldestPolicy

抛弃队列中等待最久的任务,然后把当前任务加入队列中

ThreadPoolExecutor.CallerRunsPolicy

有主线程负责调用任务的run()方法从而绕过线程池直接执行(由于主线程停下来执行任务,在该任务执行完毕前无法接收后面的任务)

import java.util.concurrent.*;

public class ThreadPoolTest1 {
    public static void main(String[] args) {
//        //1.通过ThreadPoolExecutor创建一个线程池对象
        ExecutorService pool = new ThreadPoolExecutor(3,5,8,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(4),
                Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());

        Runnable target = new MyRunnable();
        pool.execute(target);//线程池会自动创建一个新线程,自动处理这个任务
        pool.execute(target);//线程池会自动创建一个新线程,自动处理这个任务
        pool.execute(target);//线程池会自动创建一个新线程,自动处理这个任务

        pool.execute(target);//进入任务队列等待
        pool.execute(target);//进入任务队列等待
        pool.execute(target);//进入任务队列等待
        pool.execute(target);//进入任务队列等待

        pool.execute(target);//创建临时线程处理
        pool.execute(target);//创建临时线程处理

        pool.execute(target);//丢弃任务,抛出异常

//        pool.shutdown();//等待线程池里的任务全部执行完毕后,关闭线程池
//        pool.shutdownNow();//立即关闭线程池,不管任务是否执行完毕

    }
}

并发、并行

进程

正在运行的程序(软件)就是一个独立的进程

线程是属于进程的,一个进程中可以同时运行很多个线程

进程中的多个线程其实是并发和并行同时执行的

并发

进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉二这些线程在同时执行,这就是并发

并行

在同一个时刻上,同时有多个线程在被CPU调度执行,这就是并行

线程的生命周期

线程的生命周期

就是线程从生到死的过程中,经历的各种状态及状态转换

Java线程的状态

Java总共定义了6种状态,这6种状态都定义在Thread类的内部枚举类中

线程状态

说明

New(新建)

线程刚被创建,但是并未启动

Runnable(可运行)

线程已经调用了start(),等待CPU调度

Blocked(锁阻塞)

线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态

Waiting(无限等待)

一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒

Timed Waiting(计时等待)

同Waiting状态,有几个方法(sleep,wait)有超时参数,调用他们将进入Waiting状态,过了指定时间后会自动启动

Teminated(被终止)

因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡

线程的6种状态互相转换

拓展:悲观锁、乐观锁

悲观锁

一上来就加锁,没有安全感,每次只能一个线程进入访问完毕后,在解锁。线程安全,性能较差

乐观锁

一开始不上锁,大家一起跑,等要出现线程安全的时候才开始控制。线程安全,性能较好

原理

1.在线程开始执行的时候,会先存储这个值的地址

2.存储这个值,称为原始值

3.然后对这个值进行修改并存储修改后的结果(没有给这个值赋值,只是存储了修改后的值),称为修改值

4.通过地址找到这个值现在是多少并和存储的原始值比较

5.如果相等,代表这个值没有被其他线程修改,立刻将修改值赋值给这个值,然后返回结果

6.如果不相等,代表这个值已经被其他线程修改过了,之前的修改值是不正确的,舍弃修改值和存储值,重新执行第二步,直到原始值和通过地址找到的这个值相等

例子

需求:一个变量,初始值为10,对这个变量加1,且有多个线程同时执行该任务

步骤:1.存储这个值的对象和地址(这个变量是一个对象中的实例变量,因此要存储对象和地址才能找到这个变量)

2.通过地址找到这个值,并用一个变量存储起来(后面称为原始值,方便讲述)

3.对这个值进行加一,并把加一后的值用一个变量存储(称为修改值)

4.通过地址找到这个值并和原始值比较

5.如果相等,立刻将修改值赋值给这个变量,返回结果的时候使用原始值进行处理返回(不能再通过地址找值,因为这个值可能已经又被其他线程处理)(也可以使用修改值直接返回,但这是一个Java的方法的原理,里面的修改值都是,原始值经过过处理,当作形参给下一个方法,因此没有存储修改值)

6.如果不相等,再次执行第二步,直到成功修改(例子中使用了do-while方法)

该例子的实际代码

以上例子是Java中AtomicInteger的一个方法incrementAndGet,以下是实际代码

AtomicInteger a = new AtomicInteger();
a.incrementAndGet();
public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
//这是通过C语言完成的底部逻辑,不用管
//大致作用是通过var1对象和var2地址来找到要修改的值
//var5是之前存储的原始值
//var4是要修改多少,这里传过来的是1,即要修改的值加1
//如果通过地址找到的值和原始值相等,会将var5 + var4通过地址赋值给要修改的值
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值