Java基础进阶之线程详解

进程和线程:

  • 进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1–n个线程。(进程是资源(CPU,内存)分配的基本单位,是静态的概念)

  • 线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是程序运行的最小单位,真正干活的)

进程中至少会有一个线程

并行和并发:

  • 并行:多个CPU同时执行多个任务。比如:多个人同时在做不同的事。(是同时进行,同时执行某些任务)

  • 并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。(是指同时发生了,程序支持并发而已,任务同时发生了)

在单核CPU情况下,并不是真正的多线程,只不过是CPU在多个线程间进行快速切换,用户就认为线程在同时运行而已;

线程创建的三种方式

在java中要想实现多线程,有三种手段,一种是继承Thread类,另外一种是实现Runable接口,还有一种是实现Callable接口,并与Future、线程池结合使用。推荐使用实现Runable接口

1、自定义类继承Thread,重写run方法

public class MyThreadA extends Thread {

    @Override
    public void run() {
        // 线程的执行逻辑
        for (int i = 0; i < 20; i++) {
            System.out.println("线程A:" + i);
        }
    }
}

测试类

public class ThreadDemo {
    public static void main(String[] args) {
        System.out.println("main 线程开始了");
        //创建线程对象
        Thread threadA = new MyThreadA();
        //start 只是OS调度器 线程准备好了
        threadA.start();
        //threadA.run();
        System.out.println("main 线程结束了");
    }
}

注意:start()方法调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的。

main方法其实也是一个线程。在java中所有的线程都是同时启动的,至于什么时候,哪个先执行,完全看谁先得到CPU的资源。

2、实现Runnable() 接口

  • 匿名内部类
  • lambda表达式
public class Test {
    public static void main(String[] args) {
    	//匿名内部类
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    System.out.println("线程A:" + i);
                }
            }
        });
		//lambda表达式
        Thread threadB = new Thread(() -> {
            for (int i = 0; i < 20; i++) {
                System.out.println("线程B:" + i);
            }
        });
        threadA.start();
        threadB.start();
    }
}

实现Runnable接口比继承Thread类所具有的优势:

  • 避免了java中单继承的限制
  • 适合多个相同的程序代码的线程去处理同一个资源
  • 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
  • 线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类

3、实现Callable接口

  • 匿名内部类
  • lambda表达式

线程执行完之后如果需要得到线程的返回结果,需要使用到线程间的通信,就要实现Callable接口

public class Demo {
    public static void main(String[] args) {
        //lambda
        FutureTask futureTask1 = new FutureTask(() -> {
            for (int i = 0; i < 20; i++) {
                System.out.println("线程A:" + i);
            }
            return 1;
        });

        //匿名内部类
        FutureTask futureTask2 = new FutureTask(new Callable() {
            @Override
            public Object call() throws Exception {
                for (int i = 0; i < 20; i++) {
                    System.out.println("线程B:"+i);
                }
                return 100;
            }
        });


        Thread threadA = new Thread(futureTask1);
        Thread threadB = new Thread(futureTask2);
        threadA.start();
        threadB.start();

        try {
            //get() 方法是阻塞时的方法,必须等线程执行完了才会得到返回值
            Object o1 = futureTask1.get();
            System.out.println("线程A的返回值:"+o1);
            Object o2 = futureTask2.get();
            System.out.println("线程B的返回值:"+o2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("线程A 执行完了");
        System.out.println("线程B 执行完了");
    }
}

线程的常用方法

/**
 * @author Zjy
 * @date 2021/1/7 10:58
 * 创建线程的第二种方式:
 * 1、实现Runnable() 接口
 * 1.1 匿名内部类
 * 1.2 lambda表达式
 */
public class Demo {
    public static void main(String[] args) {
        //匿名内部类
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    System.out.println("线程A:" + i);
                }
            }
        }, "ThreadA");

        //lambda表达式
        Thread threadB = new Thread(() -> {
            for (int i = 0; i < 20; i++) {
                System.out.println("线程B:" + i);
            }
        }, "ThreadB");

        System.out.println("线程A的状态:" + threadA.getState());
        threadA.start();
        System.out.println("线程A的状态:" + threadA.getState());
        threadB.start();

        try {
            //当前线程休眠1s
            Thread.sleep(1000L);
            TimeUnit.SECONDS.sleep(1L);
            //返回一个当前线程
            Thread thread = Thread.currentThread();
            //获取线程名
            String name = thread.getName();
            //获取线程id
            long id = thread.getId();
            System.out.println("线程id:" + id);
            //当前线程执行结束了,其他线程才能继续执行
            //thread.join();
            //让出CPU时间片,给其他线程进行调度
            // Thread.yield();
            //获取线程的状态
            System.out.println("线程A的状态:" + threadA.getState());
            // 优先级
            int priority = threadA.getPriority();
            System.out.println("线程A的优先级:" + priority);//优先级默认都是5
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Java线程有优先级,优先级高的线程会获得较多的运行机会。

Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量:

static int MAX_PRIORITY		线程可以具有的最高优先级,取值为10static int MIN_PRIORITY		线程可以具有的最低优先级,取值为1static int NORM_PRIORITY	分配给线程的默认优先级,取值为5

Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。(一般情况下,不会手动设置优先级)

每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY。

线程池

在一个应用程序中,我们需要多次使用线程,也就意味着,我们需要多次创建并销毁线程。而创建并销毁线程的过程势必会消耗内存。而在Java中,内存资源是及其宝贵的,所以,我们就提出了线程池的概念。

线程池:Java中开辟出了一种管理线程的概念,这个概念叫做线程池,从概念以及应用场景中,我们可以看出,线程池的好处,就是可以方便的管理线程,也可以减少内存的消耗。

那么,我们应该如何创建一个线程池呢?Java中已经提供了创建线程池的一个类:Executor

频繁创建线程和销毁线程会消耗系统资源;使用线程池可以节省系统资源

创建线程池:

  • Executor.newFixedThreadPool():生成一个固定数量线程的线程池
  • Executor.newCachedThreadPool():创建一个可缓存的线程池

Executors.newCachedThreadPool()

如果线程池的大小超过了处理任务所需要的线程, 那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

		//newFixedThreadPool生成一个固定数量线程的线程池
        //ExecutorService pool = Executors.newFixedThreadPool(10);

        ExecutorService pool = Executors.newCachedThreadPool();
        pool.submit(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    System.out.println(i);
                }
                System.out.println("当前线程的id:" + Thread.currentThread().getId());
            }
        });
        pool.submit(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println(i);
            }
            return 1;
        });
        //把线程池关闭
        pool.shutdown();

线程安全问题

在多线程情况下,同时操作主存中某个数据的时候,出现数据不一致情况

创建一个BankCount类

public class BankCount {
    private double balance;

    public double getBalance() {
        return balance;
    }
    /**
     * 存钱
     * balance = balance + money;
     * 一共经历三步:
     *  1、取出balance的值
     *  2、相加
     *  3、赋值操作
     */
    public void saveMoney(double money) {
        balance += money;
    }

    /**
     * 取钱
     */
    public void drawMoney(double money) {
        balance -= money;
    }
}

创建一个测试类

public class SafeTest {
    public static void main(String[] args) {
        BankCount bankCount = new BankCount();
        Thread threadA = new Thread(()->{
            for (int i = 0; i < 20000; i++) {
                bankCount.saveMoney(10);
            }
            System.out.println("threadA结束了");
    });
        Thread threadB = new Thread(()->{
            for (int i = 0; i < 20000; i++) {
                bankCount.drawMoney(10);
            }
            System.out.println("threadB结束了");
        });
        threadA.start();
        threadB.start();
        
        try {
            TimeUnit.SECONDS.sleep(1L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("余额:"+bankCount.getBalance());
    }
}

运行结果:
在这里插入图片描述
* 存钱 * balance = balance + money; * 一共经历三步: * 1、取出balance的值 * 2、相加 * 3、赋值操作

以上三步必须进行原子性操作,否则当多个线程同时操作一个数据时,会出现线程安全问题,导致数据不一致。那我们怎么解决的?这就需要用到线程同步:

线程同步

同步:一个任务的执行需要等待另一个任务的结束

异步:任务可以同时进行

1、synchronized关键字

synchronized关键字修饰方法

	public synchronized void saveMoney(double money) {
        balance += money;
    }

    public synchronized void drawMoney(double money) {
        balance -= money;
    }

结果:

在这里插入图片描述
synchronized关键字修饰代码块

	public  void saveMoney(double money) {
        synchronized(this){
            balance += money;
        }

    }

    public  void drawMoney(double money) {
        synchronized(this){
            balance -= money;
        }
    }

结果:
在这里插入图片描述
无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁

在静态方法中的使用 synchronized

	public synchronized static void method1(){

    }
	//synchronized在静态方法中使用需要的是类对象(class)的对象锁
    public static void method2(){
        synchronized (BankCount.class){

        }
    }

2、使用ReentrantLock()锁

  • 使用ReentrantLock锁上锁后,一定要释放锁
public class BankCount {
    private double balance;
    
    private ReentrantLock lock = new ReentrantLock();
    public double getBalance() {
        return balance;
    }
	public void saveMoney(double money) {
        //上锁
        lock.lock();
        try {
            balance += money;
        } catch (Exception e) {
        } finally {
            //释放锁,一定要执行
            lock.unlock();
        }
    }

    public void drawMoney(double money) {

        lock.lock();
        try {
            balance -= money;
        } catch (Exception e) {
        } finally {
            lock.unlock();
        }
    }
    }

jdk中独占锁的实现除了使用关键字synchronized外,还可以使用ReentrantLock。虽然在性能上ReentrantLock和synchronized没有什么区别,但ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。

  • 1.ReentrantLock和synchronized都是独占锁,只允许线程互斥的访问临界区。但是实现上两者不同:synchronized加锁解锁的过程是隐式的,用户不用手动操作,优点是操作简单,但显得不够灵活。一般并发场景使用synchronized的就够了;ReentrantLock需要手动加锁和解锁,且解锁的操作尽量要放在finally代码块中,保证线程正确释放锁。ReentrantLock操作较为复杂,但是因为可以手动控制加锁和解锁过程,在复杂的并发场景中能派上用场。
  • 2.ReentrantLock和synchronized都是可重入的。synchronized因为可重入因此可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁;而ReentrantLock在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获得该锁。

区别:

  • ReentrantLock可以实现公平锁。公平锁是指当锁可用时,在锁上等待时间最长的线程将获得锁的使用权。而非公平锁则随机分配这种使用权。和synchronized一样,默认的ReentrantLock实现是非公平锁,因为相比公平锁,非公平锁性能更好。当然公平锁能防止饥饿,某些情况下也很有用。
  • ReentrantLock可响应中断:当使用synchronized实现锁时,阻塞在锁上的线程除非获得锁否则将一直等待下去,也就是说这种无限等待获取锁的行为无法被中断。而ReentrantLock给我们提供了一个可以响应中断的获取锁的方法lockInterruptibly()。该方法可以用来解决死锁问题。

公平锁和非公平锁该如何选择

大部分情况下我们使用非公平锁,因为其性能比公平锁好很多。但是公平锁能够避免线程饥饿,某些情况下也很有用

死锁

多个线程间出现互相等待对方释放锁的情况,就是死锁

解决:嵌套上锁的时候,上锁的顺序保持一致

synchronized 死锁

public class DeadLock {
    public static void main(String[] args) {
        Object o1 = new Object();
        Object o2 = new Object();
        new Thread(()->{
            synchronized (o1){
                System.out.println("线程A获取到1号锁");
                try {
                    TimeUnit.SECONDS.sleep(1L);
                    synchronized (o2){
                        System.out.println("线程A获取到2号锁");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        new Thread(()->{
            synchronized (o2){
                System.out.println("线程B获取到2号锁");
                try {
                    TimeUnit.SECONDS.sleep(1L);
                    synchronized (o1){
                        System.out.println("线程B获取到1号锁");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

    }
}

结果:
在这里插入图片描述
上锁顺序保持一致,可以避免死锁情况的发生

ReentrantLock()死锁

public class DeadLockTest {
    public static void main(String[] args) {
        Object o1 = new Object();
        Object o2 = new Object();
        ReentrantLock lock1 = new ReentrantLock();
        ReentrantLock lock2 = new ReentrantLock();

        new Thread(()->{
            lock1.lock();
            System.out.println("线程A获取到1号锁");
            try {
                TimeUnit.SECONDS.sleep(1L);
                lock2.lock();
                System.out.println("线程A获取到2号锁");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock2.unlock();
            lock1.unlock();
        }).start();
        new Thread(()->{
            lock2.lock();
            System.out.println("线程B获取到2号锁");
            try {
                TimeUnit.SECONDS.sleep(1L);
                lock1.lock();
                System.out.println("线程B获取到1号锁");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock1.unlock();
            lock2.unlock();
        }).start();
    }
}

结果:
在这里插入图片描述

使用tryLock

		new Thread(()->{
            lock1.tryLock();
            System.out.println("线程A获取到1号锁");
            try {
                TimeUnit.SECONDS.sleep(1L);
                lock2.tryLock();
                System.out.println("线程A获取到2号锁");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock2.unlock();
            lock1.unlock();
        }).start();
        new Thread(()->{
            lock2.tryLock();
            System.out.println("线程B获取到2号锁");
            try {
                TimeUnit.SECONDS.sleep(1L);
                lock1.tryLock();
                    System.out.println("线程B获取到1号锁");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock1.unlock();
            lock2.unlock();
        }).start();

结果:
在这里插入图片描述

tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。

线程安全的类

  • Vector
  • CopyOnWriteArrayList
  • AtomicInteger
public class SafetyDemo {
    public static void main(String[] args) {
        //ArrayList<Integer> integers = new ArrayList<>();//线程不安全
        //Vector<Integer> integers = new Vector<>();//线程安全
        //CopyOnWriteArrayList也是线程安全的类
        CopyOnWriteArrayList<Integer> integers = new CopyOnWriteArrayList<>();
        
        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                integers.add(i);
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                integers.add(i);
            }
        }).start();
        try {
            TimeUnit.SECONDS.sleep(1L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(integers.size());
    }
}
public class Demo {
    private static AtomicInteger count = new AtomicInteger(0);
    public static void main(String[] args) {
        test();
    }
    public static void test() {
        Thread threadA = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                count.incrementAndGet();//递增
            }
        });
        Thread threadB = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                count.decrementAndGet();//递减
            }
        });
        threadA.start();
        threadB.start();
        try {
            TimeUnit.SECONDS.sleep(1L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("count的值是:" + count);
    }
}

结果:
在这里插入图片描述

线程通信(生产者与消费者模式)

生产者与消费者模式,只是一种现象,一种问题。

多线程环境下,我们经常需要多个线程的并发和协作。这个时候,就需要了解一个重要的多线程并发协作模型“生产者/消费者模式”。

  • 什么是生产者呢?
    生产者是负责生产数据的模块(这里的模块可能是:方法、对象、线程、进程)
  • 什么是消费者呢?
    消费者是负责处理数据的模块(这里的模块可能是:方法、对象、线程、进程)
  • 什么又是缓冲区?
    消费者不能直接使用生产者的数据,它们之间有个“缓冲区”。生产者将生产好的数据放入“缓冲区”,消费者从“缓冲区”拿要处理的数据。

在这里插入图片描述
缓冲区是实现并发的核心,缓冲区的设置有以下3个好处:

1、实现线程的并发协作

  • 有了缓冲区以后,生产者线程只需要往缓冲区里面放置数据,而不需要管消费者消费的情况;同样,消费者只需要从缓冲区拿数据处理即可,也不需要管生产者生产的情况。 这样,就从逻辑上实现了“生产者线程”和“消费者线程”的分离。

2、解耦了生产者和消费者

  • 生产者不需要和消费者直接打交道。

3、解决忙闲不均,提高效率

  • 生产者生产数据慢时,缓冲区仍有数据,不影响消费者消费;消费者处理数据慢时,生产者仍然可以继续往缓冲区里面放置数据 。

代码演示:

创建一个仓库类

  • 生产商品方法
  • 消费商品方法
public class Store {
    //记录商品数量
    private int count;
    private final int MAX_COUNT = 100;

    /**
     * 生产商品
     */
    public synchronized void product() {
        while (count >= MAX_COUNT) {
            System.out.println("仓库满了,生产者需要等待");
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        count++;
        System.out.println("生产商品,当前数量是:" + count);
        this.notifyAll();
    }

    /**
     * 消费商品
     */
    public synchronized void pop() {
        while (count <= 0) {
            System.out.println("仓库空了,消费者需要等待");
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        count--;
        System.out.println("消费商品,当前数量是:" + count);
        this.notifyAll();
    }
}

测试类:

		Store store = new Store();

        Thread product = new Thread(() -> {
            for (int i = 0; i < 200; i++) {
                store.product();
            }
        });

        Thread pop = new Thread(() -> {
            for (int i = 0; i < 200; i++) {
                store.pop();
            }
        });

        product.start();
        pop.start();

结果:
在这里插入图片描述
线程通信通常用于生产者/消费者模式,情景如下:

  1. 生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。
  2. 对于生产者,没有生产产品之前,消费者要进入等待状态。而生产了产品之后,又需要马上通知消费者消费。
  3. 对于消费者,在消费之后,要通知生产者已经消费结束,需要继续生产新产品以供消费。
  4. 在生产者消费者问题中,仅有synchronized是不够的。
  • synchronized可阻止并发更新同一个共享资源,实现了同步;
  • synchronized不能用来实现不同线程之间的消息传递(通信)。

那线程是通过哪些方法来进行消息传递(通信)的呢?见如下总结:

在这里插入图片描述

notify和notifyAll的区别?

notifyAll()会唤醒所有的线程,notify()只会随机唤醒一个线程。notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而 notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。

sleep和wait方法的区别?
1、sleep是线程的静态方法,wait是Object的方法
2、sleep可以在任何地方使用,wait只能在同步方法或者同步代码块中使用
3、sleep不释放对象锁,wait方法会释放对象锁

多线程的三个核心概念

1、原子性

一个操作要么全部执行,要么全部不执行;银行转账案例,都要符合原子性

​synchronized可以满足原子性

2、可见性

多个线程访问共享变量的时候,一个线程对共享变量的操作,其他线程可见

synchronized可以保证可见性

​volatile可以保证可见性

3、顺序性

JVM底层有指令重排序,代码执行顺序不一定就是你认为的顺序;单线程情况下绝对没有问题;

​执行过程根据程序代码的顺序来进行;

​volatile可以保证顺序性,防止JVM进行指令重排序

volatile关键字

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

  • 禁止进行指令重排序。

volatile只能修饰成员变量

volatile保证可见性

private volatile static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while (flag){

            }
        }).start();

        TimeUnit.SECONDS.sleep(1L);
        flag = false;
        System.out.println("主线程结束了");
    }

结果:
在这里插入图片描述

volatile保证顺序性

见常用设计模式的单例模式:

常用设计模式之单例模式—volatile保证顺序性

总结

1、线程同步的目的是为了保护多个线程访问一个资源时对资源的破坏。

2、线程同步方法是通过锁来实现的,每个对象都有且仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他非同步方法

3、对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。

4、对于同步,要时刻清醒在哪个对象上同步,这是关键。

5、编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对“原子”操作做出分析,并保证原子操作期间别的线程无法访问竞争资源。

6、当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。

7、死锁是线程间相互等待锁造成的,在实际中发生的概率非常的小。但是,一旦程序发生死锁,程序将死掉。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值