多线程介绍

多线程介绍,第1张

线程介绍

文章目录
  • 多线程
    • 一、并发编程的优缺点
    • 二、基本概念
      • 2.1阻塞与非阻塞
      • 2.2同步与异步
      • 2.3临界区
      • 2.4并发与并行
      • 2.5线程和进程
      • 2.6上下文切换
    • 三、创建线程的四种方式
      • 3.1继承Thread类
      • 3.2实现Runnable接口
      • 3.3使用Callable和Future创建线程
      • 3.4使用Executor框架创建线程池
    • 四、线程的状态
    • 五、线程状态的基本 *** 作
      • 5.1 interrupted
      • 5.2 join
      • 5.3 sleep
      • 5.4 yield
    • 六、线程死锁
      • 6.1 产生死锁的四个必要条件
      • 6.2 如何避免死锁
    • 七、线程池
      • 7.1 为什么使用线程池
      • 7.2 线程池的分类和作用
        • newCachedThreadPool
        • newFixedThreadPool
        • newSingleThreadExecutor
        • newScheduleThreadPool
        • newSingleThreadScheduledExecutor
      • 7.3 线程池创建和参数说明
      • 7.4 阻塞队列
    • 八、Lock锁
    • 九、volatile
      • 9.1 并发编程三要素
      • 9.2 volatile关键字的两层语义
      • 9.3 volatile保证可见性
      • 9.4 volatile不保证原子性
      • 9.5 volatile能保证有序性吗?
      • 9.6 volatile关键字的使用场景

多线程 一、并发编程的优缺点

优点:

​ 1.充分利用多核cpu的计算能力

​ 2.方便进行业务拆分,提高系统并发能力和性能

缺点:

  1. 频繁的上下文切换

    解决上下文切换的解决方案

    • 无锁并发编程:可以参照concurrentHashMap锁分段的思想,不同的线程处理不同段的数据,这样在多线程竞争的条件下,可以减少上下文切换的时间。
    • CAS算法:利用Atomic下使用CAS算法来更新数据,使用了乐观锁,可以有效的减少一部分不必要的锁竞争带来的上下文切换。
    • 使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多的线程,这样会造成大量的线程都处于等待状态。
    • 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
  2. 线程安全

  3. 死锁

    1. 避免一个线程同时获得多个锁;
    2. 避免一个线程在锁内部占有多个资源,尽量保证每个锁只占用一个资源;
    3. 尝试使用定时锁,使用lock.tryLock(timeOut),当超时等待时当前线程不会阻塞;
    4. 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况
  4. 内存泄漏

二、基本概念 2.1阻塞与非阻塞

阻塞与非阻塞也就是在等待消息的时候,当前进/线程是挂起状态,还是非挂起状态。

  • 阻塞:调用在发出去后,在消息返回之前,当前进/线程会被挂起,直到有消息返回,当前进/线程才会被激活;
  • 非阻塞:调用在发出去后,不会阻塞当前进/线程,而会立即返回。
2.2同步与异步
  • 同步:当一个同步调用发出去后,调用者要一直等待调用结果的返回后,才能进行后续的 *** 作。

  • 异步:当一个异步调用发出去后,调用者不用管被调用方法是否完成,都会继续执行后面的代码。

2.3临界区

临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每个线程使用时,一旦临界区资源被一个线程占有,那么其他线程必须等待

2.4并发与并行
  • 并发:同一时间段,多个任务交替执行 (单位时间内不一定同时执行)
  • 并行:单位时间内,多个任务同时执行。真正意义上的“同时进行”,真正的并行只能出现在拥有多个CPU的系统中
2.5线程和进程
  • 进程:正在运行的程序
  • 线程:线程是进程中的一个执行单元(路径)
    • 单线程:正在运行的程序只有一个一个执行路径
    • 多线程:正在执行的程序有多个执行路径
2.6上下文切换

多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。

三、创建线程的四种方式 3.1继承Thread类

步骤:

1.写一个Thread的子类
2.复写run方法
3.创建子类对象
4.调用start()方法,开启线程

public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 1; i < 3; i++) {
            System.out.println(getName() + ":" + i);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class MyThreadTest {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.setName("t1");
        t2.setName("t2");
        t3.setName("t3");

        t1.start();
        try {
            //解释:调用join方法,等待线程t执行完毕
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();
        t3.start();
    }
}   

执行结果:

t1:1
t1:2
t2:1
t3:1
t3:2
t2:2
3.2实现Runnable接口

步骤:

1.写一个Runnable接口的子类
2.复写run方法
3.创建Thread对象,把Runnable接口的子类对象作为参数传递
4.调用start()方法,开启线程

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 2; i++) {
            System.out.println(Thread.currentThread().getName()+"..."+i);
        }
    }
}

public class MyRunnableTest {
    public static void main(String[] args) {
        MyRunnable my = new MyRunnable();
        Thread t1 = new Thread(my);
        Thread t2 = new Thread(my);
        t1.start();
        t2.start();
    }
}

测试结果:

Thread-1...0
Thread-0...0
Thread-1...1
Thread-0...1
3.3使用Callable和Future创建线程

步骤:

1.写一个Callable接口的子类
2.复写call方法。有返回值
3.创建Callable的子类对象
4.创建FutureTask对象,把Callable的子类对象作为参数传递
5.创建Thread对象,把FuTureTask对象作为参数传递
6.调用start方法,开启线程

注意:如果想的到线程的执行结果,在start方法调用之后,调用FuTureTask对象的get方法

public class MyCallable implements Callable {
    @Override
    public String call() throws Exception {
        for (int i = 0; i < 3; i++) {
            System.out.println("表白"+i);
        }
        return "接受";
    }
}

public class MyCallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable my = new MyCallable();
        FutureTask ft = new FutureTask(my);
        Thread t1 = new Thread(ft);
        t1.start();
        System.out.println(ft.get());
    }
}

测试结果:

表白0
表白1
表白2
接受
3.4使用Executor框架创建线程池
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " run()方法执行中...");
    }
}

public class SingleThreadExecutorTest {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        MyRunnable runnableTest = new MyRunnable();
        for (int i = 0; i < 3; i++) {
            executorService.execute(runnableTest);
        }
        System.out.println("线程任务开始执行");
        executorService.shutdown();
    }
}

测试结果:

线程任务开始执行
pool-1-thread-1 run()方法执行中...
pool-1-thread-1 run()方法执行中...
pool-1-thread-1 run()方法执行中...
四、线程的状态
  1. NEW(新建状态): ⾄今尚未启动的线程处于这种状态 ,还没有调用start方法
  2. RUNNABLE(就绪状态):正在 Java 虚拟机中执⾏的线程处于这种状态 ,调用了start方法,还没有抢夺CPU的执行权
  3. BLOCKED(阻塞状态):表示线程阻塞于锁
  4. WAITING(等待状态):⽆限期地等待另⼀个线程来执⾏某⼀特定 *** 作的线程处于这种状态,需要notify,或者notifyAll来唤醒
  5. TIMED_WAITING(计时等待):调⽤了sleep⽅法的状态
  6. TERMINATED(结束状态):已退出的线程处于这种状态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9tFlEhAo-1637913578522)(并发编程.assets/image-20211126135601924.png)]

五、线程状态的基本 *** 作 5.1 interrupted

中断可以理解为线程的一个标志位,它表示了一个运行中的线程是否被其他线程进行了中断 *** 作

方法名解释备注public void interrupt()中断该线程对象如果该线程被调用了Object wait/Object wait(long),或者被调用sleep(long),join()/join(long)方法时会抛出interruptedException并且中断标志位将会被清除public boolean isinterrupted()测试该线程对象是否被中断中断标志位不会被清除public static boolean interrupted()测试当前线程是否被中断中断标志位会被清除
public class InterruptDemo {
    public static void main(String[] args) throws InterruptedException {
        //sleepThread睡眠1000ms
        final Thread sleepThread = new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                super.run();
            }
        };
        //busyThread一直执行死循环
        Thread busyThread = new Thread() {
            @Override
            public void run() {
                while (true) {

                }
            }
        };

        sleepThread.start();
        busyThread.start();
        sleepThread.interrupt();
        busyThread.interrupt();
        while (sleepThread.isInterrupted()) {
        }
        System.out.println("sleepThread isInterrupted: " + sleepThread.isInterrupted());
        System.out.println("busyThread isInterrupted: " + busyThread.isInterrupted());
    }
}

测试结果:

sleepThread isInterrupted: false
busyThread isInterrupted: true
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.hl.thread.Thread.demo5.InterruptDemo.run(InterruptDemo.java:10)

另外,同样可以通过中断的方式实现线程间的简单交互, while (sleepThread.isInterrupted()) 表示在Main线程中会持续监测sleepThread线程,一旦sleepThread的中断标志位清零,即sleepThread.isInterrupted()返回为false时才会继续Main线程才会继续往下执行。因此,中断 *** 作可以看做线程间一种简便的交互方式。

5.2 join

如果一个线程实例A执行了threadB.join(),其含义是:当前线程A会等待threadB线程终止后threadA才会继续执行。

方法名解释备注public final void join() throws InterruptedException等待这个线程死亡。如果任何线程中断当前线程,如果抛出InterruptedException异常时,当前线程的中断状态将被清除public final void join(long millis) throws InterruptedException等待这个线程死亡的时间最多为millis毫秒。 0的超时意味着永远等待。如果millis为负数,抛出IllegalArgumentException异常public final void join(long millis, int nanos) throws InterruptedException等待最多millis毫秒加上这个线程死亡的nanos纳秒。如果millis为负数或者nanos不在0-999999范围抛出IllegalArgumentException异常
public class JoinDemo {
    public static void main(String[] args) {
        JoinThread j1 = new JoinThread();
        JoinThread j2 = new JoinThread();
        j1.start();
        try {
            j1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        j2.start();
    }

    static class JoinThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 3; i++) {
                System.out.println(Thread.currentThread().getName() + " terminated.");
            }
        }
    }
}

测试结果:

Thread-0 terminated.
Thread-0 terminated.
Thread-0 terminated.
Thread-1 terminated.
Thread-1 terminated.
Thread-1 terminated.

当第一个线程结束后第二个线程才执行

5.3 sleep

public static native void sleep(long millis)方法显然是Thread的静态方法,很显然它是让当前线程按照指定的时间休眠,其休眠时间的精度取决于处理器的计时器和调度器。需要注意的是如果当前线程获得了锁,sleep方法并不会失去锁

sleep()和wait()的区别

  1. sleep()方法是Thread的静态方法,而wait是Object实例方法
  2. wait()方法必须要在同步方法或者同步块中调用,也就是必须已经获得对象锁。而sleep()方法没有这个限制可以在任何地方使用。另外,wait()方法会释放占有的对象锁,使得该线程进入等待池中,等待下一次获取资源。而sleep()方法只是会让出CPU并不会释放掉对象锁;
  3. sleep()方法在休眠时间达到后,如果再次获得CPU时间片就会继续执行,而wait()方法必须等待Object.notift/Object.notifyAll通知后,才会离开等待池,并且再次获得CPU时间片才会继续执行
5.4 yield

public static native void yield()这是一个静态方法,一旦执行,它会是当前线程让出CPU,然后与当前线程具有相同优先级的线程能够获得释放出来的CPU时间片。而sleep()交出来的时间片其他线程都可以去竞争,也就是说都有机会获得当前线程让出的时间片

六、线程死锁

死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)

public class DeadLockDemo {
    private static Object resource1 = new Object();//资源 1
    private static Object resource2 = new Object();//资源 2

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 1").start();

        new Thread(() -> {
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + "get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "线程 2").start();
    }
}

测试结果:

Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1

线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过Thread.sleep(1000);让线程 A 休眠 1s 为的是让线程 B 得到CPU执行权,然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁

6.1 产生死锁的四个必要条件
  1. 互斥条件:线程(进程)对于所分配到的资源具有排它性,即一个资源只能被一个线程(进程)占用,直到被该线程(进程)释放
  2. 请求与保持条件:一个线程(进程)因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程(进程)已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  4. 循环等待条件:当发生死锁时,所等待的线程(进程)必定会形成一个环路(类似于死循环),造成永久阻塞
6.2 如何避免死锁

我们只要破坏产生死锁的四个条件中的其中一个就可以了。

  • 破坏互斥条件:这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)
  • 破坏请求与保持条件:一次性申请所有的资源
  • 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源
  • 破坏循环等待条件:
七、线程池 7.1 为什么使用线程池
  • 降低资源消耗。通过复用已存在的线程和降低线程关闭的次数来尽可能降低系统性能损耗;
  • 提升系统响应速度。通过复用线程,省去创建线程的过程,因此整体上提升了系统的响应速度;
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,因此,需要使用线程池来管理线程。
7.2 线程池的分类和作用 newCachedThreadPool

作用:创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们,并在需要时使用提供的 ThreadFactory 创建新线程。

特征:
(1)线程池中数量没有固定,可达到最大值(Interger. MAX_VALUE)
(2)线程池中的线程可进行缓存重复利用和回收(回收默认时间为1分钟)
(3)当线程池中,没有可用线程,会重新创建一个线程

创建方式: Executors.newCachedThreadPool();

newFixedThreadPool
作用:创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。

特征: 
(1)线程池中的线程处于一定的量,可以很好的控制线程的并发量 
(2)线程可以重复被使用,在显示关闭之前,都将一直存在 
(3)超出一定量的线程被提交时候需在队列中等待

创建方式: 
(1)Executors.newFixedThreadPool(int nThreads);//nThreads为线程的数量 
(2)Executors.newFixedThreadPool(int nThreads,ThreadFactory threadFactory);//nThreads为线程的数量,threadFactory创建线程的工厂方式
newSingleThreadExecutor
作用:创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。(注意,如果因为在关闭前的执行期间出现失败而终止了此单个线程,那么如果需要,一个新线程将代替它执行后续的任务)。可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。与其他等效的 newFixedThreadPool(1) 不同,可保证无需重新配置此方法所返回的执行程序即可使用其他的线程。

特征: 
(1)线程池中最多执行1个线程,之后提交的线程活动将会排在队列中以此执行

创建方式: 
(1)Executors.newSingleThreadExecutor() ; 
(2)Executors.newSingleThreadExecutor(ThreadFactory threadFactory);// threadFactory创建线程的工厂方式
newScheduleThreadPool
作用: 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

特征: 
(1)线程池中具有指定数量的线程,即便是空线程也将保留 
(2)可定时或者延迟执行线程活动

创建方式: 
(1)Executors.newScheduledThreadPool(int corePoolSize);// corePoolSize线程的个数 
(2)newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory);// corePoolSize线程的个数,threadFactory创建线程的工厂
newSingleThreadScheduledExecutor
作用: 创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期地执行。

特征: 
(1)线程池中最多执行1个线程,之后提交的线程活动将会排在队列中以此执行 
(2)可定时或者延迟执行线程活动

创建方式: 
(1)Executors.newSingleThreadScheduledExecutor() ; 
(2)Executors.newSingleThreadScheduledExecutor(ThreadFactory threadFactory) ;//threadFactory创建线程的工厂
7.3 线程池创建和参数说明
  • 静态方法创建线程池
public class MyThreadPoolDemo01 {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        //Executors------可以帮助我们新建线程池对象
        //ExecutorService-------可以帮助我们控制线程池对象
        executorService.submit(() -> {
            System.out.println(Thread.currentThread().getName() + "执行了");
        });
        executorService.submit(() -> {
            System.out.println(Thread.currentThread().getName() + "执行了");
        });
        executorService.shutdown();
    }
}
  • 自定义线程池
public class MyThreadPoolDemo02 {
    public static void main(String[] args) {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                2, //最大核心数量
                5,//最大线程数量
                2,//临时线程等待的时间
                TimeUnit.SECONDS,//时间单位
                new ArrayBlockingQueue<>(10),//创建任务队列
                Executors.defaultThreadFactory(),//创建默认线程工厂
                new ThreadPoolExecutor.AbortPolicy());//创建拒绝策略,超过了最大处理量就不执行了
        for (int i = 0; i < 17; i++) {
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"执行了");
                }
            });
        }
        pool.shutdown();
    }
}
  • 参数介绍
ThreadPoolExecutor(int corePoolSize,
                   int maximumPoolSize,
                   long keepAliveTime,
                   TimeUnit unit,
                   BlockingQueue workQueue,
                   ThreadFactory threadFactory,
                   RejectedExecutionHandler handler)
参数说明:
    corePoolSize:表示核心线程池的大小。当提交一个任务时,如果当前核心线程池的线程个数没有达到corePoolSize,即使当前核心线程池有空闲的线程,也会创建新的线程来执行所提交的任务。如果当前核心线程池的线程个数已经达到了corePoolSize,则不再重新创建线程。如果调用了prestartCoreThread()或者 prestartAllCoreThreads(),线程池创建的时候所有的核心线程都会被创建并且启动
    
    maximumPoolSize:表示线程池能创建线程的最大个数。如果当阻塞队列已满时,并且当前线程池线程个数没有超过maximumPoolSize的话,就会创建新的线程来执行任务
    
    keepAliveTime:空闲线程存活时间。如果当前线程池的线程个数已经超过了corePoolSize,并且线程空闲时间超过了keepAliveTime的话,就会将这些空闲线程销毁,这样可以尽可能降低系统资源消耗
    
    unit:时间单位。为keepAliveTime指定时间单位
    
    workQueue:阻塞队列。用于保存任务的阻塞队列。可以用ArrayBlockingQueue, 
    	linkedBlockingQueue, SynchronousQueue, PriorityBlockingQueue
    
    threadFactory:创建线程的工厂类。可以通过指定线程工厂为每个创建出来的线程设置更有意义的名字,如果出现并发问题,也方便查找问题原因。
    
    handler:拒绝策略。当线程池的阻塞队列已满和指定的线程都已经开启,说明当前线程池已经处于饱和状态了,那么就需要采用一种策略来处理这种情况。采用的策略有这几种:
    1.AbortPolicy: 直接拒绝所提交的任务,并抛出RejectedExecutionException异常
    2.CallerRunsPolicy:只用调用者所在的线程来执行任务;
    3.DiscardPolicy:不处理直接丢弃掉任务
    4.DiscardOldestPolicy:丢弃掉阻塞队列中存放时间最久的任务,执行当前任务
7.4 阻塞队列
  • ArrayBlockingQueue:一个用数组实现的有界阻塞队列,按照先入先出(FIFO)的原则对元素进行排序。不保证线程公平访问队列,使用较少

  • PriorityBlockingQueue:支持优先级的无界阻塞队列,使用较少

  • linkedBlockingQueue:一个用链表实现的有界阻塞队列,队列默认和最长长度为Integer.MAX_VALUE。队列按照先入先出的原则对元素进行排序,使用较多。吞吐量通常要高于 ArrayBlockingQueue。Executors.newFixedThreadPool() 使用了这个队列

  • SynchronousQueue:不储存元素(无容量)的阻塞队列,每个put *** 作必须等待一个take *** 作,否则不能继续添加元素。支持公平访问队列,常用于生产者,消费者模型,吞吐量较高,使用较多每个插入 *** 作必须等到另一个线程调用移除 *** 作,否则插入 *** 作一直处于阻塞状态
    吞吐量通常要高于 linkedBlockingQueue。Executors.newCachedThreadPool使用了这个队列

八、Lock锁

锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源。在Lock接口出现之前,java程序主要是靠synchronized关键字实现锁功能的,而java SE5之后,并发包中增加了lock接口,它提供了与synchronized一样的锁功能。虽然它失去了像synchronize关键字隐式加锁解锁的便捷性,但是却拥有了锁获取和释放的可 *** 作性,可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。通常使用显示使用lock的形式如下:

Lock lock = new ReentrantLock();
lock.lock();
try {
	.......
} finally {
    lock.unlock();
}

注意:synchronized同步块执行完成或者遇到异常是锁会自动释放,而lock必须调用unlock()方法释放锁,因此在finally块中释放锁。

Lock接口API

//获取锁
void lock(); 
//获取锁的过程能够响应中断
void lockInterruptibly() throws InterruptedException;
//非阻塞式响应中断能立即返回,获取锁放回true反之返回fasle
boolean tryLock();
//超时获取锁,在超时内或者未中断的情况下能够获取锁
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//获取与lock绑定的等待通知组件,当前线程必须获得了锁才能进行等待,进行等待时会先释放锁,当再次获取锁时才能从等待中返回
Condition newCondition();
// 释放锁。
unlock();
九、volatile 9.1 并发编程三要素
  1. 原子性:即一个 *** 作或者多个 *** 作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
  2. 可见性:可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
  3. 有序性:即程序执行的顺序按照代码的先后顺序执行
9.2 volatile关键字的两层语义
  • 保证了不同线程对这个变量进行 *** 作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
  • 禁止进行指令重排序
9.3 volatile保证可见性

当多线程访问共享数据的时候,不是直接对主内存的数据进行 *** 作的,而是在线程本地内存创建一个和主内存一样的变量副本,对副本进行赋值,然后在重新赋值给主内存。

由于cpu的随机性,可能出现一个线程修改了主内存的数据,但是另外一个线程没有获取到主内存中最新的值,使用volatile修饰这个变量,就可以解决这个问题。强制让线程每次使用变量时,都从主内存中获取最新的值

9.4 volatile不保证原子性
public class Test {
    public volatile int inc = 0;
     
    public void increase() {
        inc++;
    }
     
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                @Override
                public void run() {
                    for(int j=0;j<1000;j++) {
                        test.increase();
                    }
                };
            }.start();
        }
         
        while(Thread.activeCount()>1){//保证前面的线程都执行完
            Thread.yield();
        }
        System.out.println(test.inc);
    }
}

这里的结果怎么都不是10000,因为volatile关键字能保证可见性没有错,但是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的 *** 作的原子性(自增不是原子 *** 作)

解决方式:

采用synchronized:

public class Test {
    public  int inc = 0;
    
    public synchronized void increase() {
        inc++;
    }
    
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
        
        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

采用Lock:

public class Test {
    public  int inc = 0;
    Lock lock = new ReentrantLock();
    
    public  void increase() {
        lock.lock();
        try {
            inc++;
        } finally{
            lock.unlock();
        }
    }
    
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
        
        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

采用AtomicInteger:

public class Test {
    public  AtomicInteger inc = new AtomicInteger();
     
    public  void increase() {
        inc.getAndIncrement();
    }
    
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
        
        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}
9.5 volatile能保证有序性吗?

volatile能在一定程度上保证有序性

volatile关键字禁止指令重排序有两层意思:

  1. 当程序执行到volatile变量的读 *** 作或者写 *** 作时,在其前面的 *** 作的更改肯定全部已经进行,且结果已经对后面的 *** 作可见;在其后面的 *** 作肯定还没有进行;
  2. 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
9.6 volatile关键字的使用场景

synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证 *** 作的原子性。通常来说,使用volatile必须具备以下2个条件:

  • 对变量的写 *** 作不依赖于当前值
  • 该变量没有包含在具有其他变量的不变式中

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5611968.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-15
下一篇 2022-12-15

发表评论

登录后才能评论

评论列表(0条)

保存