Java—多线程篇

多线程

并发(Concurrent):在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。

并行(Parallel):当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。其实决定并行的因素不是CPU的数量,而是CPU的核心数量,比如一个CPU多个核也可以并行。

进程(Process):进程是正在运行的程序实体,并且包含这个运行的程序中占占据的所有系统资源。

**线程 **(Thread):线程是一个程序内部的一条执行路径,它是操作系统能够进行运算调度的最小单位,它被包含在进程中实际运作单位,一条线程指的是进程中一个单一顺序的控制流,一个进程中可以进行多个线程,每条线程并行执行不同的任务。

如果一个进程中同时运行了多个线程(多个线程交替占用CPU资源,而非真正的并行执行),用来完成不同的工作,则称之为多线程

1、多线程优点
  • 充分利用CPU的资源
  • 简化编程模型
  • 带来良好的用户体验
2、缺点
  • 对线程进行管理要求额外的 CPU开销,线程的使用会给系统带来上下文切换的额外负担。
3、多线程的创建方式
3.1继承Thread类

优点:编码简单

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

package com.csi;

public class ThreadTest extends Thread{

    @Override
    public void run() {

        for (int i = 0; i < 5; i++) {

            System.out.println(Thread.currentThread().getName() + "在执行");

        }
    }

    public static void main(String[] args) {

        Thread thread1 = new ThreadTest();
        Thread thread2 = new ThreadTest();

        thread1.start();
        thread2.start();

    }
}

/**
Thread-0在执行
Thread-1在执行
Thread-0在执行
Thread-1在执行
Thread-0在执行
Thread-1在执行
Thread-0在执行
Thread-1在执行
Thread-0在执行
Thread-1在执行
*/

运行结果每次不一定相同

3.2实现Runnable接口

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

缺点:编程多一层对象的包装,如果线程有执行结果不能直接返回

public class MyRunnable implements Runnable{
    @Override
    public void run() {

        for (int i = 0; i < 5; i++) {

            System.out.println(Thread.currentThread().getName() + "在执行");

        }

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

        Runnable myRunnable1 = new MyRunnable();
        Runnable myRunnable2 = new MyRunnable();

        Thread thread1 = new Thread(myRunnable1);
        Thread thread2 = new Thread(myRunnable2);

        thread1.start();
        thread2.start();

    }
}
/*
Thread-1在执行
Thread-1在执行
Thread-0在执行
Thread-1在执行
Thread-0在执行
Thread-1在执行
Thread-0在执行
Thread-1在执行
Thread-0在执行
Thread-0在执行
*/
3.3实现Callable接口

优点:如果线程有执行结果可以直接返回,线程任务类只是实现任务接口,可以继续继承其他类和实现其他的接口,扩展性强

缺点:编码复杂

public class CallableTest implements Callable<Integer> {

    private int count;
    @Override
    public Integer call() throws Exception {

        for (int i = 1; i <= 100; i++) {

            count += i;

        }
        return count;
    }
}
package com.csi;

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

public class TestCallable {

    public static void main(String[] args) {

        Callable call = new CallableTest();

        FutureTask futureTask = new FutureTask(call);

        Thread thread = new Thread(futureTask);

        thread.start();

        try {
            Object count = futureTask.get();
            System.out.println(count);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        }

    }
}
//5050
3.4实现Runnable接口和继承Thread的区别

继承Thread一个类只能继承一个父类,存在局限;一个类可以实现多个接口,扩展性增强

4.线程的调度

请添加图片描述

  • setPriority(int newPriority):可以调节1~10,线程优先级默认为5
  • sleep(long millis):线程进入休眠状态,并且不释放锁(若有锁),休眠结束后直接进入就绪状态
  • yield():使CPU暂时放弃对该线程的调度,执行其他的线程,但是不一定暂停。
  • interrupt():终端当前线程
  • isAlive():测试当前线程是否存活,返回boolean值
5、线程的同步

当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常。

5.1同步方法

即有synchronized关键字修饰的方法。 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。

5.2同步代码块

即有synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步

6、等待和唤醒

public void wait() : 让当前线程进入到等待状态 此方法必须锁对象调用

public void notify() : 唤醒当前锁对象上等待状态的线程 此方法必须锁对象调用.

案例:包子铺制作包子后唤醒消费者线程,随后进入休眠状态,消费者线程买完包子,唤醒包子铺线程制作包子,瑞后进入休眠状态

//包子铺类
public class BunStore implements Runnable{

    private List list;

    public BunStore(List list) {
        this.list = list;
    }

    @Override
    public void run() {

        while (true) {
            synchronized (list) {
                if (list.size() > 0){

                    try {
                        System.out.println("等待消费者消费包子");
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                list.add(0,"包子");

                System.out.println("生产了一个包子");

                list.notify();
            }
        }

    }
}
//消费者类
public class Custormer implements Runnable{

    private List list;

    public Custormer(List list) {
        this.list = list;
    }
    @Override
    public void run() {

        while (true) {
            synchronized (list) {
                if (list.size() == 0){

                    try {
                        System.out.println("等待生产者生产包子");
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }
                //消费包子
                list.remove(0);

                System.out.println("消费了一个包子");

                list.notify();
            }
        }
    }
}
//测试类
public class Test {

    public static void main(String[] args) {

        List list = new ArrayList();
        BunStore bunStore = new BunStore(list);
        Custormer custormer = new Custormer(list);

        Thread thread1 = new Thread(bunStore);
        Thread thread2 = new Thread(custormer);

        thread1.start();
        thread2.start();
        
    }
}
7、volatile关键字

volatile也是java提供的一种轻量级的同步机制,但是volatile只能保证可见性,无法保证其原子性

public class VolatileThread extends Thread{

    private volatile boolean flag = false;

    private boolean isFlag(){
        return flag;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        this.flag = true;
        System.out.println("flag=" + flag);
    }

    public static void main(String[] args) {

        VolatileThread volatileThread = new VolatileThread();
        volatileThread.start();

        while (true){
            if (volatileThread.isFlag()){
                System.out.println("执行了");
            }
        }

    }
}

若没有volatile 修饰则主线程中无法获取flag修改过后的值,则以一直在死循环中

使用场景:

利用可见性特点,控制某一段代码执行或者关闭

多个线程操作共享变量,但是是有一个线程对其进行写操作,其他的线程都是读

8、JMM

概述:JMM(Java Memory Model)Java内存模型,是java虚拟机规范中所定义的一种内存模型。

Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将

变量存储到内存和从内存中读取变量这样的底层细节。

所有的共享变量都存储于主内存。这里所说的变量指的是实例变量和类变量。不包含局部变量,因为局部变

量是线程私有的,因此不存在竞争问题。每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存完成。

9、AtomicInteger关键字

在多线程环境下,volatile关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性(在多线程环境下volatile修饰的变量也是线程不安全的)

public class VolatileAtomicThread implements Runnable{

    private volatile int count = 0 ;
    private static final Object obj = new Object();


    @Override
    public void run() {

        for(int x = 0 ; x < 100 ; x++) {
            synchronized (obj) {
                count++ ;
                System.out.println("count= " + count);
            }
        }
    }

    public static void main(String[] args) {
        VolatileAtomicThread vat = new VolatileAtomicThread();
        Thread thread1 = new Thread(vat);
        Thread thread2 = new Thread(vat);
        thread1.start();
        thread2.start();

    }
}
10、原子类CAS机制实现线程安全。

CAS的全称是: Compare And Swap(比较再交换); 是现代CPU广泛支持的一种对内存中的共享数据进行操作

的一种特殊指令。CAS可以将read-modify-check-write

转换为原子操作,这个原子操作直接由处理器保证。

CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

举例:

  1. 在内存地址V当中,存储着值为10的变量。

请添加图片描述

  1. 此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11。

请添加图片描述

  1. 在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。

    请添加图片描述

  2. 线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。

请添加图片描述

  1. 线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。

请添加图片描述

  1. 这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。

请添加图片描述

  1. 线程1进行SWAP,把地址V的值替换为B,也就是12。

请添加图片描述

11、ConcurrentHashMap

为什么要使用ConcurrentHashMap?

  1. HashMap线程不安全,会导致数据错乱

  2. 使用线程安全的Hashtable效率低下

    public class Const extends Thread{
        public static HashMap<String,String> map = new HashMap<>();
    
        public void run(){
    
            for (int i = 0; i < 500000; i++) {
                Const.map.put(this.getName() + (i + 1), this.getName() + i + 1);
            }
    
        }
    
        public static void main(String[] args) {
            Const a1 = new Const();
            Const a2 = new Const();
            a1.setName("线程1-");
            a2.setName("线程2-");
            a1.start();
            a2.start();
            //休息10秒,确保两个线程执行完毕
            try {
                Thread.sleep(1000 * 5);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //打印集合大小
            System.out.println("Map大小:" + Const.map.size());
        }
    }
    
    //Map大小:943870
    

    两个线程分别向同一个map中写入50000个键值对,最后map的size应为:100000

    为了保证线程安全,可以使用ConcurrentHashMap

    package com.csi;
    
    import java.util.concurrent.ConcurrentHashMap;
    
    public class Const extends Thread{
        public static ConcurrentHashMap<String,String> map = new ConcurrentHashMap<>();
    
        public void run(){
    
            for (int i = 0; i < 500000; i++) {
                Const.map.put(this.getName() + (i + 1), this.getName() + i + 1);
            }
    
        }
    
        public static void main(String[] args) {
            Const a1 = new Const();
            Const a2 = new Const();
            a1.setName("线程1-");
            a2.setName("线程2-");
            a1.start();
            a2.start();
            //休息10秒,确保两个线程执行完毕
            try {
                Thread.sleep(1000 * 5);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //打印集合大小
            System.out.println("Map大小:" + Const.map.size());
        }
    }
    
    
    /*
    Map大小:1000000
    */
    

    ConcurrentHashMap能保证结果正确,而且提高了效率。

12、CountDownLatch

CountDownLatch允许一个或多个线程等待其他线程完成操作,再执行自己。

例如:线程1要执行打印:A和C,线程2要执行打印:B,但线程1在打印A后,要线程2打印B之后才能打印

C,所以:线程1在打印A后,必须等待线程2打印完B之后才能继续执行。

ThreadAC类

package com.csi;

import java.util.concurrent.CountDownLatch;

public class ThreadAC extends Thread{
    private CountDownLatch down ;
    public ThreadAC(CountDownLatch down) {
        this.down = down;
    }
    @Override
    public void run() {
        System.out.println("A");
        try {
            down.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("C");
    }
}

ThreadB类

package com.csi;

import java.util.concurrent.CountDownLatch;

public class ThreadB extends Thread{

    private CountDownLatch down ;
    public ThreadB(CountDownLatch down) {
        this.down = down;
    }
    @Override
    public void run() {
        System.out.println("B");
        down.countDown();
    }
}

测试类

package com.csi;

import java.util.concurrent.CountDownLatch;

public class Demo {
    public static void main(String[] args) {
        CountDownLatch down = new CountDownLatch(1);//创建1个计数器
        new ThreadAC(down).start();
        new ThreadB(down).start();
    }
}

//结果
/*
A
B
C
*/

会保证按:A B C的顺序打印。

CountDownLatch是通过一个计数器来实现的,每当一个线程完成了自己的任务后,可以调用countDown()

方法让计数器-1,当计数器到达0时,调用CountDownLatch。

await()方法的线程阻塞状态解除,继续执行。

13、CyclicBarrier

CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一

个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线

程才会继续运行。

使用场景:CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。

需求:使用两个线程读取2个文件中的数据,当两个文件中的数据都读取完毕以后,进行数据的汇总操作。

14、Semaphore

Semaphore(发信号)的主要作用是控制线程的并发数量。

synchronized可以起到"锁"的作用,但某个时间段内,只能有一个线程允许执行。

Semaphore可以设置同时允许几个线程执行。

Semaphore字面意思是信号量的意思,它的作用是控制访问特定资源的线程数目。

public Semaphore(int permits) permits 表示许可线程的数量

public Semaphore(int permits, boolean fair) fair 表示公平性,如果这个设为 true的话,下次执行的线程会是等待最久的线程

示例:同时允许2个线程同时执行

Service类:

package com.csi;

import java.util.concurrent.Semaphore;

public class Service {
    private Semaphore semaphore = new Semaphore(2);
    public void testMethod() {
        try {
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName()
                    + " 进入 时间=" + System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println(Thread.currentThread().getName()
                    + " 结束 时间=" + System.currentTimeMillis());
            semaphore.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

线程类:

package com.csi;

public class ThreadA extends Thread{

    private Service service;
    public ThreadA(Service service) {
        super();
        this.service = service;
    }
    @Override
    public void run() {
        service.testMethod();
    }
}

测试类

package com.csi;

public class DemoSemaphore {
    public static void main(String[] args) {
        Service service = new Service();
        //启动5个线程
        for (int i = 1; i <= 5; i++) {
            ThreadA a = new ThreadA(service);
            a.setName("线程 " + i);
            a.start();//5个线程会同时执行Service的testMethod方法,而某个时间段只能有1个线程执行
        }
    }
}
//结果
/*
线程 3 进入 时间=1689601225566
线程 1 进入 时间=1689601225566
线程 3 结束 时间=1689601230590
线程 1 结束 时间=1689601230590
线程 5 进入 时间=1689601230590
线程 4 进入 时间=1689601230590
线程 5 结束 时间=1689601235591
线程 2 进入 时间=1689601235591
线程 4 结束 时间=1689601235605
线程 2 结束 时间=1689601240593
*/
15、Exchanger

Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。

这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程

也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据

传递给对方。

使用场景

使用场景:可以做数据校对工作

需求:比如我们需要将纸制银行流水通过人工的方式录入成电子银行流水。为了避免错误,采用AB岗两人进

行录入,录入到两个文件中,系统需要加载这两个文件,

并对两个文件数据进行校对,看看是否录入一致

16、线程池

线程池其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

请添加图片描述

16.1优点:
  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。

  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

16.2线程池的使用

Java里面线程池的顶级接口是 java.util.concurrent.Executor ,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService 。

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在 java.util.concurrent.Executors 线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象。

16.3创建线程池

使用线程池中线程对象的步骤:

  1. 创建线程池对象。

  2. 创建Runnable接口子类对象。(task)

  3. 提交Runnable接口子类对象。(take task)

  4. 关闭线程池(一般不做)。

17、死锁

概念:在多线程程序中,使用了多把锁,造成线程之间相互等待.程序不往下走了,尽量避免死锁

17.1产生死锁的条件
  1. 有多把锁
  2. 有多个线程
  3. 有同步代码块嵌套
17.2死锁代码
public class Demo05 {
	public static void main(String[] args) {
		MyRunnable mr = new MyRunnable();
        
		new Thread(mr).start();
		new Thread(mr).start();
	}
}
class MyRunnable implements Runnable {
	Object objA = new Object();
	Object objB = new Object();
    
	/*
	嵌套1 objA
	嵌套1 objB
	嵌套2 objB
	嵌套1 objA
	*/
    
@Override
public void run() {
	synchronized (objA) {
		System.out.println("嵌套1 objA");
		synchronized (objB) {// t2, objA, 拿不到B锁,等待
			System.out.println("嵌套1 objB");
		}
	}
	synchronized (objB) {
		System.out.println("嵌套2 objB");
		synchronized (objA) {// t1 , objB, 拿不到A锁,等待
			System.out.println("嵌套2 objA");
			}
		}
	}
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值