多线程简介和线程同步

一、相关概念

1.1 线程的概念

        线程(Thread)是一个程序内部的一条执行流程。程序中如果只有一条执行流程,那么这个程序就是单线程的程序。

1.2 多线程的概念

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

1.3 注意事项

        1、启动线程必须是调用 start() 方法,不是调用 run() 方法。调用 run() 方法还属于单线程。

        2、不要把主线程的任务放在子线程的任务之前。

二、创建线程的三种方式

2.1 继承 Thread 类

        定义一个子类 MyThread 集成线程类 Thread,重写 run() 方法。然后调用线程对象的 start() 方法启动线程。

public class MyThread extends Thread{

	@Override
	public void run() {
		System.out.println("MyThread");
	}
}
public class Run {

    // main 方法是由一条默认的主线程负责执行
	public static void main(String[] args) {
		MyThread myThread = new MyThread();
		myThread.start();
		System.out.println("运行结束!");
	}
}

2.1.1 优点

        编码简单。

2.1.2 缺点 

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

2.2 实现 Runnable 接口

        定义一个线程任务类 MyRunnable 实现 Runnable 接口,重写 run() 方法。创建 MyRunnable 任务对象,把任务对象交给 Thread 处理,最后调用线程对象的 start() 方法启动线程。

// 1、定义一个任务类,实现 Runnable 接口
public class MyRunnable implements Runnable{

    // 2、重写 run() 方法
	@Override
	public void run() {
        // 线程要执行的任务
		System.out.println("运行中!");
	}
}
public class Test {
    public static void main(String[] args) {
        // 3、创建任务对象
        MyRunnable myRunnable = new MyRunnable();

        // 4、把任务对象交给一个线程对象处理
        new Thread(myRunnable).start();
    }
}

        也可以通过创建 Runnable 的匿名内部类的方式创建线程。

public class Test {
    public static void main(String[] args) {
        Runnable target = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i <5 ; i++) {
                    System.out.println("子线程输出:"+i);
                }
            }
        };
        new Thread(target).start();
        for (int i = 0; i <5 ; i++) {
            System.out.println("main 线程输出:"+i);
        }
    }
}

        还可以采用更简化的写法,如下所示: 

public class Test {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i <5 ; i++) {
                    System.out.println("子线程输出:"+i);
                }
            }
        }).start();
        for (int i = 0; i <5 ; i++) {
            System.out.println("main 线程输出:"+i);
        }
    }
}

        还可以采用 lambda 表达式的写法,如下所示:

public class Test {
    public static void main(String[] args) {
        new Thread( () -> {
                for (int i = 0; i <5 ; i++) {
                    System.out.println("子线程输出:"+i);
                }
        }).start();
        for (int i = 0; i <5 ; i++) {
            System.out.println("main 线程输出:"+i);
        }
    }
}

2.2.1 优点

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

2.2.2 缺点

        需要多创建一个 Runnable 对象。

2.3 实现 Callable 接口

        前两种线程创建方式都存在一个问题:假如线程执行完毕后有一些数据需要返回,他们重写的 run() 方法均不能直接返回结果。

        在 jdk5.0 中提供了 Callable FutureTask 类解决上面的问题,可以返回线程执行完毕的结果。

// 1、实现 Callable 接口,并指定要返回的泛型类型
public class MyCallable implements Callable<String> {
    private int n;
    public MyCallable(int n) {
        this.n = n;
    }
    // 2、重写 call 方法
    @Override
    public String call() throws Exception {
        // 3、描述线程的任务,返回线程执行后的结果
        int sum = 0;
        for(int i=0;i<=n;i++){
            sum +=i;
        }
        return "线程求和的结果为:"+sum;
    }
}
public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 3、创建一个 Callable 对象
        Callable<String> callable = new MyCallable(100);

        // 4、把 callable 对象封装成一个 FutureTask 对象(任务对象)
        // 这个 FutureTask 为未来任务对象,它实现了 Runnable 接口
        // 可以在线程执行完毕后,用未来任务对象的 get 方法获取线程执行完毕后的结果
        FutureTask<String>  f1 = new FutureTask<>(callable);

        // 5、把任务对象交给 Thread 对象
        new Thread(f1).start();
        // 6、获取线程执行完毕后返回的结果
        // 注意:如果主线程执行到这,上面的线程还没有执行完,则 main 线程阻塞
        // 即下面的代码会暂停,等待上面线程执行完毕后才会继续执行
        String s = f1.get();
        System.out.println(s);
    }
}

 2.3.1 优点

        线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。可以在线程执行完毕后去获取线程执行的结果。

2.3.2 缺点

        编码复杂一点。

三、Thread 类的常用方法

# 获取当前线程的名字
Thread.currentThread().getName()

# 获取当前对象的名字,默认为 Thread-0
this.getName()

# 设置当前对象的名字
this.setName(String name);

# 设置线程休眠时间,单位为毫秒
this.sleep(long time)

# 让当前调用这个方法的线程先执行完
this.join()

# 获取当前线程的唯一标识
this.getId()

# 放弃当前的CPU资源,将它让给其他的任务去占用 CPU 执行时间,但放弃的时间不确定,有可能刚刚放弃,马上又获得 CPU 时间片
this.yield()

# 设定线程的优先级
this.setPriority()

# 设置线程为守护线程,且必须在线程启动前设置,当main线程运行完,守护线程也就停止了
this.setDaemon()

# 测试当前线程是否已经中断(静态方法),线程的中断状态由该方法清除
this.interrupted()

# 判断线程是否已经中断(boolean类型),不清除状态标志
this.isInterrupted()

四、线程安全

4.1 线程安全问题概念

        多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题。

4.2 线程安全出现的原因

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

4.3 案例分析

4.3.1 需求

        小明和小红是一对夫妻,他们有一个共同的账户,余额是 10万元,模拟 2 人同时去取 10万元。

4.3.2 分析

        1、需要提供一个账户类,接着创建一个账户对象代表两人的共享账户

        2、需要定义一个线程类(用于创建两个线程,分别代表小明和小红)。

        3、创建 2 个线程,传入同一账户对象给 2 个线程处理。

        4、启动 2 个线程,同时去同一个账户对象中取钱 10 万。

4.3.3 代码演示

        首先创建一个账户类,代码如下所示:

public class Account {
    // 余额
    private double money;
    public Account(){
    }
    public Account(double money) {
        this.money = money;
    }
    // setter 和 getter 
    public void drawMoney(double money){
        String name = Thread.currentThread().getName();
        if(this.money >= money){
            System.out.println(name + "来取钱"+money+"成功!");
            this.money -= money;
            System.out.println(name + "来取钱后,剩余的前为"+this.money);
        }else{
            System.out.println(name+"来取钱,钱不够");
        }
    }
}

        然后创建一个线程类,如下所示:

public class DrawThread extends Thread{

    private Account account;
    public DrawThread(Account account,String name){
        super(name);
        this.account = account;
    }
    @Override
    public void run() {
        // 取钱(小明和小红)
        account.drawMoney(100000);
    }
}

        测试的代码如下:

public class AccountTest {
    public static void main(String[] args) {
        // 1、创建一个账户对象,代表两人的共同财产
        Account acc = new Account(100000);

        // 2、创建两个线程,分别代表小明和小红,再去同一个账户对象中取钱
        new DrawThread(acc,"小明").start();// 小明
        new DrawThread(acc,"小红").start();// 小红
    }
}

        打印的内容如下所示,可以发现出现了线程安全问题。

五、线程同步

5.1 概念

        解决线程安全问题的方案。

5.2 思想

        让多个线程实现先后依次访问共享资源,这样就解决了安全问题。

5.3 线程同步的常见方案

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

5.4 方式一:同步代码块

5.4.1 作用

        把访问共享资源的核心代码给上锁,以此保证线程安全。语法如下:

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

5.4.2 原理

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

5.4.3 注意事项

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

5.4.4 代码修改

        修改核心代码类 Account,添加同步代码块,如下所示:

public class Account {
    // 余额
    private double money;
    public Account(){
    }
    public Account(double money) {
        this.money = money;
    }
    // setter 和 getter
    public void drawMoney(double money){
        String name = Thread.currentThread().getName();
        // 对于当前执行的线程(小明线程和小红线程)来说,必须是同一个对象才可以的。
        // 这块我们随便写一个字符串对象 "我爱你" 就可以充当锁,因为这个字符串对象是在常量池里面,且只有一份
        // 对于小明线程和小红线程来说,"我爱你" 肯定是同一个对象,当然可以锁住小明或者小红线程。只允许一个线程进来
        synchronized ("我爱你") {
            if(this.money >= money){
                System.out.println(name + "来取钱"+money+"成功!");
                this.money -= money;
                System.out.println(name + "来取钱后,剩余的前为"+this.money);
            }else{
                System.out.println(name+"来取钱,钱不够");
            }
        }
    }
}

        执行结果如下所示:

5.4.5 代码块原理

        当小明线程和小红线程执行到同步代码块的时候,他们就开始竞争这个锁对象,这个锁对象是 ”我爱你“,即便是两个人同时过来,锁对象也只会交给一个人来占有。

        假设是小明先占有这个锁对象,他就会在这个锁对象的底层打上一个标记,代表这个对象已经被人加锁了。其他的线程就只能在外面等着不能进来了。等到小明线程执行结束后就把锁释放掉了。

        解锁之后,小红线程就发现锁对象被人解锁了,即标记没有了,它就会把这个锁对象占据。它就会给这个锁对象打上加锁的标记,添加完之后它就进来了。

        这样就实现了线程安全了。

5.4.6 锁对象深究

        在上面的代码中,我们使用的锁对象为任意的唯一对象,这样写是不好的,此时我们又重新创建了两个线程,如下所示:

public class AccountTest {
    public static void main(String[] args) {
        // 1、创建一个账户对象,代表两人的共同财产
        Account acc = new Account(100000);
        // 2、创建两个线程,分别代表小明和小红,再去同一个账户对象中取钱
        new DrawThread(acc,"小明").start();// 小明
        new DrawThread(acc,"小红").start();// 小红

        // 1、创建一个账户对象,代表两人的共同财产
        Account acc2 = new Account(100000);
        // 2、创建两个线程,分别代表小黑和小白,再去同一个账户对象中取钱
        new DrawThread(acc2,"小黑").start();// 小黑
        new DrawThread(acc2,"小白").start();// 小白
    }
}

        此时系统里面有 4 条线程,小明和小红线程是一家人,小黑和小白线程是一家人。小明和小红去他们家的账户取钱,而小黑和小白去他们家的账户取钱。

        而此时我们同步代码里面的锁对象 ”我爱你“ 在系统里面是只有一份的,这样就会导致对 ”我爱你“ 的锁对象来说:小明、小红、小黑和小白都是同一个对象。这四个线程只要有一个线程获取锁,其他的三个线程就只能等待,这样是不合理的。

        我们希望的是小明抢到锁之后,只需要锁住小红就可以,不能锁住小黑或者小白。之所以出现这个问题是因锁对象的范围太大了。

        我们想要的是小明和小红是一个锁对象,小黑和小白是一个锁对象,互不干扰。那该怎么办呢?官方建议我们使用共享资源作为锁,this 就可以,如下:

public void drawMoney(double money){
	String name = Thread.currentThread().getName();
	synchronized (this) {
		if(this.money >= money){
			System.out.println(name + "来取钱"+money+"成功!");
			this.money -= money;
			System.out.println(name + "来取钱后,剩余的前为"+this.money);
		}else{
			System.out.println(name+"来取钱,钱不够");
		}
	}
}

5.4.7 静态方法和锁对象

        如果是静态方法,官方建议的锁对象为当前类的 ”类名.class“ 。这个 ”类名.class“ 为字节码文件,这个字节码文件在我们的系统中是只有一份的。

public static void test(){
	synchronized (Account.class){
		// todo
	}
}

5.4.8 总结

        1、锁对象随便选择一个唯一的对象好不好呢?不好,会影响其他无关线程的执行。

        2、建议使用共享资源作为锁对象,对于实例方法建议使用 this 作为锁对象。

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

 5.5 方式二:同步方法

5.5.1 作用

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

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

    // 操作共享资源的代码
}

5.5.2 原理

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

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

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

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

5.5.3 代码修改

public synchronized void drawMoney(double money){
	String name = Thread.currentThread().getName();
		if(this.money >= money){
			System.out.println(name + "来取钱"+money+"成功!");
			this.money -= money;
			System.out.println(name + "来取钱后,剩余的前为"+this.money);
		}else{
			System.out.println(name+"来取钱,钱不够");
		}
}

5.5.4 同步方法原理

        在执行同步方法的时候,可能有多个线程进行访问。它的底层其实也是有一个隐含的锁,如果这个方法是实例方法,那么这个隐含的锁用的就是 this,只是我们看不到。

        如果我们的同步方法是个静态的方法,它隐含的锁用的其实就是 ”类名.class”

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

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

        2、可读性:同步方法好。

5.6 方式三:Lock 锁

5.6.1 作用

        Lock 锁是 jdk5 开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大。

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

5.6.2 代码修改

public class Account {
    // 余额
    private double money;
    // 1、创建一个锁对象,并使用 final 修饰,不允许其他地方修改
    private final Lock lock = new ReentrantLock();
    public Account(){
    }
    public Account(double money) {
        this.money = money;
    }
    // setter 和 getter 
    public void drawMoney(double money){
        String name = Thread.currentThread().getName();
        // 添加异常控制,防止出现异常锁得不到释放
        try {
            // 2、加锁
            lock.lock();
            if(this.money >= money){
                System.out.println(name + "来取钱"+money+"成功!");
                this.money -= money;
                System.out.println(name + "来取钱后,剩余的前为"+this.money);
            }else{
                System.out.println(name+"来取钱,钱不够");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 3、解锁
            lock.unlock();
        }
    }
}

5.7 synchronized 锁重入

        synchronized 关键字拥有锁重入的功能,也就是在使用 synchronized 时,当一个线程得到一个对象后,再次请求此对象锁时时可以再次得到该对象的锁的。

        这也证明在一个 synchronized 方法或方法块内部调用本类的其他 synchronized 方法或方法块时,是永远可以得到锁的。

        可重入锁也支持在父子类继承的环境中,即子类可以通过“可重入锁”调用父类的同步方法的。

5.8 异常自动释放锁

        当一个线程执行的代码出现异常时,其所持有的锁会自动释放。

5.9 同步不具有继承性

        同步不可以继承。

六、习题演练

6.1 习题描述

        有 100 份礼物,小红和小明两人同时发送,当剩下的礼品小于 10 份的时候不再送出,利用多线程模拟该过程并将线程的名称打印出来,并最后在控制台分别打印小红和小明各自送出多少份礼物。

6.2 习题编码

public class CupsTest {

    public static void main(String[] args) throws Exception{
        // 1、添加 100 份礼品到程序中来
        ArrayList<String> gift = new ArrayList<String>();
        for(int i=1;i<=100;i++){
            gift.add("礼物"+i);
        }
        // 2、定义线程类,创建线程对象,去集合中拿礼物给别人
        Cups cups_xm = new Cups(gift,"小明");
        Cups cups_xh = new Cups(gift,"小红");
        cups_xm.start();
        cups_xh.start();
        // 3、调用 join() 方法,等待子线程结束才能统计调用数量
        cups_xm.join();
        cups_xh.join();

        System.out.println("小明送出了:"+cups_xm.getCount()+"份礼物");
        System.out.println("小红送出了:"+cups_xh.getCount()+"份礼物");
    }
}
public class Cups extends Thread{
    private ArrayList<String> list;
    // 用于统计各自发送了多少个礼物
    private int count;
    public Cups(ArrayList<String> list,String name){
        super(name);
        this.list = list;
    }
    @Override
    public  void run() {
        String threadName= Thread.currentThread().getName();
        // list 对象相对于小明和小红线程来说是唯一的
        while(true) {
            synchronized (list){
                if(list.size()<10){
                    break;
                }
                String name = list.get(0);
                System.out.println(threadName+"线程送出了:"+name);
                list.remove(name);
                count++;
            }
        }
    }
    public int getCount() {
        return count;
    }
    public void setCount(int count) {
        this.count = count;
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

快乐的小三菊

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

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

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

打赏作者

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

抵扣说明:

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

余额充值