线程同步

当多个线程访问一个对象时,有可能会发生污读,即读取到未及时更新的数据,这个时候就需要线程同步。

线程同步:

即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态,实现线程同步的方法有很多,临界区对象就是其中一种。

在一般情况下,创建一个线程是不能提高程序的执行效率的,所以要创建多个线程。但是多个线程同时运行的时候可能调用线程函数,在多个线程同时对同一个内存地址进行写入,由于CPU时间调度上的问题,写入数据会被多次的覆盖,所以就要使线程同步。

同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。

“同”字从字面上容易理解为一起动作

其实不是,“同”字应是指协同、协助、互相配合。

如进程、线程同步,可理解为进程或线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B依言执行,再将结果给A;A再继续操作。

所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回,同时其它线程也不能调用这个方法。按照这个定义,其实绝大多数函数都是同步调用(例如sin, isdigit等)。但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。例如Window API函数SendMessage。该函数发送一个消息给某个窗口,在对方处理完消息之前,这个函数不返回。当对方处理完毕以后,该函数才把消息处理函数所返回的LRESULT值返回给调用者。

在多线程编程里面,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性。

由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可能存在以下问题:

  • 一个线程持有锁会导致其他所有需要此锁的线程挂起;
  • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引

    起性能问题;
  • 如果一个优先级高的线程等待- -个优先级低的线程释放锁会导致优先级倒

    置,引起性能问题.

举个例子,一个售票口有10张票,当100个人同时去买时,每个人都获取到了有100张票的数据,所以每个人买了一张,导致最后剩下-90张票,线程不同步就会导致这种结果。

synchronized

synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:

  1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;

  2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;

  3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;

  4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

我们写一个例子,使用线程不安全的List来看看效果

  1. public class MyThread{
  2. public static void main(String[] args) throws InterruptedException {
  3. List<String> list = new ArrayList<>();
  4. for (int i = 0; i < 1000; i++) {
  5. new Thread(()->{
  6. list.add(Thread.currentThread().getName());
  7. }).start();
  8. }
  9. Thread.sleep(2000);
  10. System.out.println(list.size());
  11. }
  12. }

可以看到,循环1000次,只存进去998个,重复执行,这个大小还会变化,所以是线程不安全的。

可以使用synchronized把list加锁,就能保证每次都能插入进去。

  1. public class MyThread{
  2. public static void main(String[] args) throws InterruptedException {
  3. List<String> list = new ArrayList<>();
  4. for (int i = 0; i < 1000; i++) {
  5. new Thread(()->{
  6. synchronized (list) {
  7. list.add(Thread.currentThread().getName());
  8. }
  9. }).start();
  10. }
  11. Thread.sleep(2000);
  12. System.out.println(list.size());
  13. }
  14. }

这样就能够保证线程安全。

也可以使用JUC(java.util.concurrent)包下的线程安全的列表CopyOnWriteArrayList,代码如下

  1. import java.util.concurrent.CopyOnWriteArrayList;
  2. public class MyThread{
  3. public static void main(String[] args) throws InterruptedException {
  4. CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
  5. for (int i = 0; i < 1000; i++) {
  6. new Thread(()->{
  7. list.add(Thread.currentThread().getName());
  8. }).start();
  9. }
  10. Thread.sleep(2000);
  11. System.out.println(list.size());
  12. }
  13. }

使用CopyOnWriteArrayList就可以不需要synchronized关键字实现线程安全

查看源代码可以发现,CopyOnWriteArrayList实现了List<E>接口

然后再add方法中使用了synchronized来加锁,和我们上面的操作方法一致

  1. //CopyOnWriteArrayList中的add()方法
  2. public boolean add(E e) {
  3. synchronized (lock) {
  4. Object[] es = getArray();
  5. int len = es.length;
  6. es = Arrays.copyOf(es, len + 1);
  7. es[len] = e;
  8. setArray(es);
  9. return true;
  10. }
  11. }

死锁

所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。

死锁的条件

  • 互斥条件
  • 请求和保持
  • 不可抢占
  • 循环等待

只要破坏后三个条件之一就可以避免死锁,可以使用银行家算法等方法。

Lock锁

  • 从JDK 5.0开始,Java提供了更强大的线程同步机制一通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。
  • Lock锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开

    始访问共享资源之前应先获得Lock对象
  • ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。

先写一个不使用锁的例子

  1. import java.util.concurrent.locks.ReentrantLock;
  2. public class MyThread implements Runnable {
  3. public static void main(String[] args) {
  4. MyThread thread = new MyThread();
  5. Thread thread1 = new Thread(thread);
  6. Thread thread2 = new Thread(thread);
  7. Thread thread3 = new Thread(thread);
  8. thread1.start();
  9. thread2.start();
  10. thread3.start();
  11. }
  12. public static int tickets = 10;
  13. @Override
  14. public void run() {
  15. while (true) {
  16. if (tickets > 0) {
  17. System.out.println(tickets--);
  18. } else {
  19. break;
  20. }
  21. }
  22. }
  23. }

执行后发现顺序完全是乱的

使用ReentrantLock(可重入锁)来把相关代码加锁,即可实现按顺序调用

  1. import java.util.concurrent.locks.ReentrantLock;
  2. public class MyThread implements Runnable {
  3. public static void main(String[] args) {
  4. MyThread thread = new MyThread();
  5. Thread thread1 = new Thread(thread);
  6. Thread thread2 = new Thread(thread);
  7. Thread thread3 = new Thread(thread);
  8. thread1.start();
  9. thread2.start();
  10. thread3.start();
  11. }
  12. public static int tickets = 10;
  13. final ReentrantLock lock = new ReentrantLock();
  14. @Override
  15. public void run() {
  16. while (true) {
  17. try {
  18. lock.lock();
  19. if (tickets > 0) {
  20. System.out.println(tickets--);
  21. } else {
  22. break;
  23. }
  24. } finally {
  25. lock.unlock();
  26. }
  27. }
  28. }
  29. }

这样也可以实现线程同步。

  • Lock是显式锁(手动开启和关闭锁,别忘记关闭锁) synchronized是隐式锁,出了

    作用域自动释放
  • Lock只有代码块锁,synchronized有代码块锁和方法锁
  • 使用Lock锁, JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展

    性(提供更多的子类)。
  • 优先使用顺序:
    • Lock >同步代码块(已经进入了方法体,分配了相应资源) >同步方法(在方

      法体之外)

线程通信

生产者和消费者问题

  • 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费。
  • 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止。
  • 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止。

Java提供的线程通信方法

方法名 作用
wait() 表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁
wait(long timeout) 指定等待的毫秒数
notify() 唤醒一个处于等待状态的线程
notifyAll() 唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度

均是0bject类的方法都,只能在同步方法或者同步代码块中使用,否则会抛出llegalMonitorStateException

  • 对于生产者,没有生产产品之前,要通知消费者等待.而生产了产品之后,又需要马_上通知消费者消费
  • 对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费
  • 在生产者消费者问题中,仅有synchronized是不够的
    • synchronized 可阻止并发更新同- -个共享资源,实现了同步
    • synchronized 不能用来实现不同线程之间的消息传递(通信)

解决方式一:管程

首先定义一个生产者类

  1. //生产者
  2. class Producer extends Thread {
  3. SynContainer container;
  4. public Producer(SynContainer container) {
  5. this.container = container;
  6. }
  7. //生产
  8. @Override
  9. public void run() {
  10. for (int i = 0; i < 100; i++) {
  11. System.out.println("生产第" + i + "个");
  12. container.push(new Product(i));
  13. }
  14. }
  15. }

生产者不断往缓冲区添加产品,然后定义一个消费者类

  1. //消费者
  2. class Consumer extends Thread {
  3. SynContainer container;
  4. public Consumer(SynContainer container) {
  5. this.container = container;
  6. }
  7. //消费
  8. @Override
  9. public void run() {
  10. for (int i = 0; i < 100; i++) {
  11. System.out.println("消费第" + container.pop().id + "个");
  12. try {
  13. Thread.sleep(500);
  14. } catch (InterruptedException ignored) { }
  15. }
  16. }
  17. }

消费者不断在缓冲区去除产品,这里添加一个sleep来模拟真实效果

最后定义缓冲区

  1. //缓冲区
  2. class SynContainer {
  3. //容器大小
  4. Product[] products = new Product[10];
  5. //计数器
  6. int count = 0;
  7. //生产者放入产品
  8. public synchronized void push(Product product) {
  9. //如果满了,通知消费者,生产者等待,否则放入产品
  10. if (count == products.length) {
  11. try {
  12. this.wait();
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. }
  17. products[count++] = product;
  18. this.notifyAll();
  19. }
  20. //消费者消费产品
  21. public synchronized Product pop() {
  22. if (count == 0) {
  23. try {
  24. this.wait();
  25. } catch (InterruptedException e) {
  26. e.printStackTrace();
  27. }
  28. }
  29. this.notifyAll();
  30. return products[--count];
  31. }
  32. }

缓冲区的两个方法都是使用synchronized修饰,保证能够执行完整,然后根据容器大小来判断是否让生产者以及消费者线程等待

当容器中没有产品时,通知消费者等待,生产者线程开始,当产品满时,通知生产者等待,消费者线程开始。

最后补上产品类

  1. //产品
  2. class Product {
  3. //产品编号
  4. int id;
  5. public Product(int id) {
  6. this.id = id;
  7. }
  8. }

解决方式二:信号量

类定义和上面类似,只不过在产品类中添加了一个信号量来区分是否有产品,不需要一个缓冲区

  1. //生产者
  2. class Producer extends Thread {
  3. Product product;
  4. public Producer(Product product) {
  5. this.product = product;
  6. }
  7. //生产
  8. @Override
  9. public void run() {
  10. for (int i = 0; i < 10; i++) {
  11. this.product.push("产品" + i);
  12. }
  13. }
  14. }
  15. //消费者
  16. class Consumer extends Thread {
  17. Product product;
  18. public Consumer(Product product) {
  19. this.product = product;
  20. }
  21. //消费
  22. @Override
  23. public void run() {
  24. for (int i = 0; i < 10; i++) {
  25. this.product.pop();
  26. }
  27. }
  28. }
  29. //产品
  30. class Product {
  31. String product;
  32. boolean flag = true;
  33. //生产
  34. public synchronized void push(String product) {
  35. if (!flag) {
  36. try {
  37. this.wait();
  38. } catch (InterruptedException ignored) { }
  39. }
  40. System.out.println("生产了" + product);
  41. //通知消费
  42. this.notifyAll();
  43. this.product = product;
  44. this.flag = !this.flag;
  45. }
  46. //消费
  47. public synchronized void pop() {
  48. if (flag) {
  49. try {
  50. this.wait();
  51. } catch (InterruptedException ignored) { }
  52. }
  53. System.out.println("消费了" + this.product);
  54. //通知生产者
  55. this.notifyAll();
  56. this.flag = !this.flag;
  57. }
  58. }

这样也可以解决生产者和消费者问题

线程池

背景

经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。

思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

优点

  • 提高响应速度(减少了创建新线程的时间)
  • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  • 便于线程管理

参数说明

  • corePoolSize: 核心池的大小
  • maximumPoolSize:最大线程数
  • keepAliveTime: 线程没有任务时最多保持多长时间后会终止

JDK 5.0起提供了线程池相关API: ExecutorService和Executors

ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor

  • void execute(Runnable command) :执行任务/命令,没有返回值,-般用来执行Runnable
  • <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一-般 又来执行

    Callable
  • void shutdown() :关闭连接池

    Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

代码演示

  1. import java.util.concurrent.ExecutorService;
  2. import java.util.concurrent.Executors;
  3. public class Test {
  4. public static void main(String[] args) {
  5. //创建线程池
  6. ExecutorService service = Executors.newFixedThreadPool(10);
  7. service.execute(new MyThread());
  8. service.execute(new MyThread());
  9. service.execute(new MyThread());
  10. //关闭连接
  11. service.shutdown();
  12. }
  13. }
  14. class MyThread implements Runnable {
  15. @Override
  16. public void run() {
  17. System.out.println(Thread.currentThread().getName());
  18. }
  19. }

这样就可以实现通过线程池来管理线程

总结

  • 线程就是独立的执行路径;
  • 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程;
  • main()称之为主线程,为系统的入口,用于执行整个程序;
  • 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与
  • 操作系统紧密相关的,先后顺序是不能认为的干预的。
  • 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制;
  • 线程会带来额外的开销,如cpu调度时间,并发控制开销。
  • 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致

Java多线程(上)https://www.cnblogs.com/chaofanq/p/15024558.html

查看原文

Java多线程(下)的更多相关文章

  1. JAVA多线程下高并发的处理经验

    java中的线程:java中,每个线程都有一个调用栈存放在线程栈之中,一个java应用总是从main()函数开始运行,被称为主线程.一旦创建一个新的线程,就会产生一个线程栈.线程总体分为:用户线程和守 ...

  2. Java 多线程下的单例模式

    单例对象(Singleton)是一种常用的设计模式.在Java应用中,单例对象能保证在一个JVM中,该对象只有一个实例存在.正是由于这个特 点,单例对象通常作为程序中的存放配置信息的载体,因为它能保证 ...

  3. [JAVA]多线程下如何确定执行顺序性

    最近在讨论一个下载任务:要求文件下载后进行打包,再提供给用户下载: 如何确保打包的线程在所有下载文件的线程执行完成后进行呢? 看看下面三个兄弟的本事: CountDownLatch.CyclicBar ...

  4. java多线程下的所的概念

    锁和synchronized关键字     为了同步多线程,Java语言使用监视器(monitors),一种高级的机制来限定某一 时刻只有一个线程执行一段受监视器保护的代码.监视器的行为是通过锁来实现 ...

  5. java多线程下如何调用一个共同的内存单元(调用同一个对象)

    /* * 关于线程下共享相同的内存单元(包括代码与数据) * ,并利用这些共享单元来实现数据交换,实时通信与必要的同步操作. * 对于Thread(Runnable target)构造方法创建的线程, ...

  6. JAVA多线程下,获取递增的序列号

    场景描述: 1,目前我们的系统可以简单归纳成MVC的架构模式 2,每个前端的请求过来,都会在C层开启事务,最后处理结束后,也在在C层关闭事务(实际是在C层的底层统一做了事务的开启和提交):      ...

  7. java多线程下模拟抢票

    我们设置三个对象分别同时抢20张票,利用多线程实现. public class Web123506 implements Runnable{ private int ticteksNums=20;// ...

  8. Java多线程(一) 多线程的基本使用

    在总结JDBC数据库连接池的时候,发现Java多线程这块掌握得不是很好,因此回头看了下多线程的内容.做一下多线程模块的学习和总结,稳固一下多线程这块的基础.关于多线程的一些理论知识,这里不想啰嗦太多, ...

  9. java多线程之线程的同步与锁定(转)

    一.同步问题提出 线程的同步是为了防止多个线程访问一个数据对象时,对数据造成的破坏. 例如:两个线程ThreadA.ThreadB都操作同一个对象Foo对象,并修改Foo对象上的数据. publicc ...

  10. java多线程学习笔记(三)

    java多线程下的对象及变量的并发访问 上一节讲到,并发访问的时候,因为是多线程,变量如果不加锁的话,会出现“脏读”的现象,这个时候需要“临界区”的出现去解决多线程的安全的并发访问.(这个“脏读”的现 ...

随机推荐

  1. 自主数据类型:在TVM中启用自定义数据类型探索

    自主数据类型:在TVM中启用自定义数据类型探索 介绍 在设计加速器时,一个重要的决定是如何在硬件中近似地表示实数.这个问题有一个长期的行业标准解决方案:IEEE 754浮点标准.1.然而,当试图通过构 ...

  2. 使用JS获取两个时间差(JS写一个倒计时功能)

    <body onload="myFunction()"> <p id="demo"></p> <script> ...

  3. 骑士CMS<6.0.48 模板注入文件包含漏洞复现及遇到的坑

    1.坑 payload:variable=1&tpl=<?php phpinfo(); ob_flush();?>/r/n<qscms/company_show 列表名=&q ...

  4. 用CLion实现本地方法并给java调用

    众所周知,PHP是世界上最好的语言,java排第二,因为PHP无所不能.但是在某些场景下java还要调用本地方法来提高执行的效率,故java只能排第二.java提供了jni(Java Native I ...

  5. mybatis之模糊查询

    1.编写接口 List<User> getUserLike(String value); 2.编写映射文件 <select id="getUserLike" re ...

  6. asp.net core配合vue实现后端验证码逻辑

    概述 网上的前端验证码逻辑总感觉不安全,验证码建议还是使用后端配合验证. 如果产品确定可以上网的话,就可以使用腾讯,百度等第三方验证,对接方便.但是产品可能内网部署,就必须自己写了. 本文章就是基于这 ...

  7. 「模拟8.19 A嚎叫..(set) B主仆..(DFS) C征程..(DP+堆优化)」

    为啥这一套题目背景感到很熟悉. T1  嚎叫响彻在贪婪的厂房 考试一个小时没调出来,自闭了.......... 正解很好想,最后实在打不出来了只好暴力骗分了... 联想到以前做的题:序列(涉及质因数分 ...

  8. NOIP模拟测试17「入阵曲&#183;将军令&#183;星空」

    入阵曲 题解 应用了一种美妙移项思想, 我们先考虑在一维上的做法 维护前缀和$(sum[r]-sum[l-1])\%k==0$可以转化为 $sum[r]\% k==sum[l-1]\%k$开个桶维护一 ...

  9. LM-MLC 一种基于完型填空的多标签分类算法

    LM-MLC 一种基于完型填空的多标签分类算法 1 前言 本文主要介绍本人在全球人工智能技术创新大赛[赛道一]设计的一种基于完型填空(模板)的多标签分类算法:LM-MLC,该算法拟合能力很强能感知标签 ...

  10. MySQL 为什么使用 B+ 树来作索引?

    什么是索引? 所谓的索引,就是帮助 MySQL 高效获取数据的排好序的数据结构.因此,根据索引的定义,构建索引其实就是数据排序的过程. 平时常见的索引数据结构有: 二叉树 红黑树 哈希表 B Tree ...