线程安全、volatile关键字、原子性、并发包、死锁、线程池

本文详细介绍了Java中解决多线程并发安全问题的方法,包括synchronized关键字的使用和Lock接口的应用。synchronized作为传统的同步机制,确保了代码块的互斥执行,但效率较低。Lock接口提供更灵活的锁机制,如ReentrantLock,能解决更复杂的并发问题。此外,还讨论了原子性、线程安全、高并发场景以及线程池的概念和使用,强调了在多线程编程中保证数据一致性的重要性。
摘要由CSDN通过智能技术生成

线程安全问题

synchronized

  • synchronized关键字:表示“同步”的。它可以对“多行代码”进行“同步”——将多行代码当成是一个完整的整体,一个线程如果进入到这个代码块中,会全部执行完毕,执行结束后,其它线程才会执行。这样可以保证这多行的代码作为完整的整体,被一个线程完整的执行完毕。

  • synchronized被称为“重量级的锁”方式,也是“悲观锁”——效率比较低。

  • synchronized有几种使用方式: a).同步代码块

    b).同步方法【常用】

当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。

要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制(synchronized)来解决。

使用synchronized关键字修饰的代码块就是同步代码块,表示只对这个区块的资源实行互斥访问

synchronized(锁对象){
    // 代码块
}

public class MyRunnable implements Runnable {

    // 共享变量 总票数
    int tickets = 100;

    @Override
    public void run() {
        // 卖票的代码
        // 循环卖票
        while (true) {
           // 加锁
            synchronized (this){
                // 如果票数小于1,就停止卖票
                if (tickets < 1){
                    break;
                }

                // 暂停
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                // 卖票
                System.out.println(Thread.currentThread().getName() + ":正在出售第" + tickets + "张票.");
                tickets--;
            }// 释放锁
        }
    }
}

public class Test {
    public static void main(String[] args) {
        // 需求:  使用多线程模拟4个窗口共同卖100张电影票
        // 创建线程任务对象
        MyRunnable mr = new MyRunnable();
        // 创建4条线程并启动
        Thread t1 = new Thread(mr, "窗口1");
        Thread t2 = new Thread(mr, "窗口2");
        Thread t3 = new Thread(mr, "窗口3");
        Thread t4 = new Thread(mr, "窗口4");
        t1.start();
        t2.start();
        t3.start();
        t4.start();

    }
}

Lock锁

  • 概述: 也是一种锁,他比synchronized更加强大,更加面向对象

  • 使用:

    • Lock是一个接口,Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作

    • 使用Lock就需要使用Lock接口的实现类ReentrantLock

    • 常用方法:

      • void lock();加锁

      • void unlock();释放锁

高并发及线程安全

  • 高并发:是指在某个时间点上,有大量的用户(线程)同时访问同一资源。例如:天猫的双11购物节、12306的在线购票在某个时间点上,都会面临大量用户同时抢购同一件商品/车票的情况。

  • 线程安全:在某个时间点上,当大量用户(线程)访问同一资源时,由于多线程运行机制的原因,可能会导致被访问的资源出现"数据污染"的问题。

多线程的运行机制

  • 原理: 抢占式调度

  • 特点: 当一个线程启动后,JVM会为其分配一个独立的"线程栈区",这个线程会在这个独立的栈区中运行。

  • Volatile关键字:

    • 概述: 它是一个修饰符,只能用来修饰成员变量

    • 作用:

      • 1.被volatile修饰的成员变量,可以强制要求线程从主内存中获取新的值

      • 2.被volatile修饰的成员变量,可以保证不会被编译器重排

    • volatile可以解决可见性,有序性问题,但是不能解决原子性问题

多线程的安全性问题-原子性

  • 原子性:所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行,多个操作是一个不可以分割的整体。

演示原子性问题 

  • 需求:一条子线程和一条主线程都对共享变量a进行++操作,每条线程对a++操作100000次

解决原子性问题

  • volatile不能解决原子性问题,只能解决可见性,有序性问题

  • 加锁---->可以解决一切问题(可见性,有序性,原子性问题)

  • 原子类:

    • 概述: java.util.concurrent.atomic包中提供了很多原子类,这些原子类在多线程的环境下是线程安全的

    • 作用: 可以解决原子性问题,可见性问题,有序性问题

    • 使用AtomicInteger类来解决这个案例的原子性问题

      • AtomicInteger类:

        • public AtomicInteger();创建一个AtomicInteger对象,表示整数0

        • public AtomicInteger(int nul);创建一个AtomicInteger对象,表示指定整数

        • public final int getAndIncrement(); 自增1

        • public final int get(); 获取当前对象表示的整数值

AtomicIntegerArray类示例

  • 常用的数组操作的原子类: 1).java.util.concurrent.atomic.AtomicIntegerArray:对int数组操作的原子类。 int[]

    2).java.util.concurrent.atomic.AtomicLongArray:对long数组操作的原子类。long[]

    3).java.util.concurrent.atomic.AtomicReferenceArray:对引用类型数组操作的原子类。Object[]

  • 多线程操作数组不安全:

    • 需求: 使用1000条线程对int数组中的每个元素自增1

多线程操作数组安全:

  • 使用AtomicIntegerArray原子类:

    • 概述: 表示一个int数组,但是是线程安全的

    • 构造方法:

      • public AtomicIntegerArray(int length);创建AtomicIntegerArray对象,指定数组长度

    • 常用方法:

      • public final int getAndAdd(int i,int delta);为指定索引的元素加dalta

      • public int length();返回数组的长度;

并发包

在JDK的并发包里提供了几个非常有用的并发容器和并发工具类。供我们在多线程开发中进行使用。

CopyOnWriteArrayList

CopyOnWriteArraySet

ConcurrentHashMap

CountDownLatch

CyclicBarrier

Semaphore

Exchanger

线程池的概念

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?

在Java中可以通过线程池来达到这样的效果。

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

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

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

线程池的使用

  • 概述: 真正的线程池接口是java.util.concurrent.ExecutorService。

  • 创建线程池:

    • Executors线程池工具类,里面提供了一些静态方法, 可以用来生成一些常用的线程池

      • public static ExecutorService newFixedThreadPool(int nThreads)获取线程池指定线程数量

  • 使用线程池: ExecutorService线程池接口: - public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行任务 - public <T> Future<T> submit(Callable<T> task):获取线程池中的某一个线程对象,并执行任务 - Future用来封装返回值,封装后的返回值可以通过Future的get()方法获取 - 对于线程池提交的任务是实现Runnable的任务,那么这个返回值Future其实没有啥用处 - 因为Future封装的是任务中run方法的返回值,而Runnable中的run方法没有返回值,所以Future没有意义 - 对于线程池提交的任务是实现Callable的任务,那么这个返回值Future就有用 - 因为Callable的任务方法: V call() 有返回值,执行完call方法的返回值会封装成Future对象返回,如果想要得到call方法的返回值,就通过Future对象调用get方法得到.

  • 线程池的使用步骤:

    • 创建线程池

    • 提交任务

    • 销毁线程池(一般不操作)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值