线程同步

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

线程同步:

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

在一般情况下,创建一个线程是不能提高程序的执行效率的,所以要创建多个线程。但是多个线程同时运行的时候可能调用线程函数,在多个线程同时对同一个内存地址进行写入,由于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来看看效果

public class MyThread{
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
Thread.sleep(2000);
System.out.println(list.size());
}
}

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

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

public class MyThread{
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
new Thread(()->{
synchronized (list) {
list.add(Thread.currentThread().getName());
}
}).start();
}
Thread.sleep(2000);
System.out.println(list.size());
}
}

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

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

import java.util.concurrent.CopyOnWriteArrayList;

public class MyThread{
public static void main(String[] args) throws InterruptedException {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 1000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
Thread.sleep(2000);
System.out.println(list.size());
}
}

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

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

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

//CopyOnWriteArrayList中的add()方法
public boolean add(E e) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
es = Arrays.copyOf(es, len + 1);
es[len] = e;
setArray(es);
return true;
}
}

死锁

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

死锁的条件

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

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

Lock锁

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

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

先写一个不使用锁的例子

import java.util.concurrent.locks.ReentrantLock;

public class MyThread implements Runnable {
public static void main(String[] args) {
MyThread thread = new MyThread();
Thread thread1 = new Thread(thread);
Thread thread2 = new Thread(thread);
Thread thread3 = new Thread(thread); thread1.start();
thread2.start();
thread3.start(); }
public static int tickets = 10;
@Override
public void run() {
while (true) {
if (tickets > 0) {
System.out.println(tickets--);
} else {
break;
}
}
}
}

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

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

import java.util.concurrent.locks.ReentrantLock;

public class MyThread implements Runnable {
public static void main(String[] args) {
MyThread thread = new MyThread();
Thread thread1 = new Thread(thread);
Thread thread2 = new Thread(thread);
Thread thread3 = new Thread(thread); thread1.start();
thread2.start();
thread3.start(); }
public static int tickets = 10;
final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
lock.lock();
if (tickets > 0) {
System.out.println(tickets--);
} else {
break;
}
} finally {
lock.unlock();
}
}
}
}

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

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

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

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

      法体之外)

线程通信

生产者和消费者问题

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

Java提供的线程通信方法

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

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

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

解决方式一:管程

首先定义一个生产者类

//生产者
class Producer extends Thread {
SynContainer container;
public Producer(SynContainer container) {
this.container = container;
} //生产
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("生产第" + i + "个");
container.push(new Product(i));
}
}
}

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

//消费者
class Consumer extends Thread {
SynContainer container;
public Consumer(SynContainer container) {
this.container = container;
} //消费
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("消费第" + container.pop().id + "个");
try {
Thread.sleep(500);
} catch (InterruptedException ignored) { }
}
}
}

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

最后定义缓冲区

//缓冲区
class SynContainer {
//容器大小
Product[] products = new Product[10];
//计数器
int count = 0; //生产者放入产品
public synchronized void push(Product product) {
//如果满了,通知消费者,生产者等待,否则放入产品
if (count == products.length) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
products[count++] = product;
this.notifyAll();
}
//消费者消费产品
public synchronized Product pop() {
if (count == 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.notifyAll();
return products[--count];
}
}

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

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

最后补上产品类

//产品
class Product {
//产品编号
int id; public Product(int id) {
this.id = id;
}
}

解决方式二:信号量

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

//生产者
class Producer extends Thread {
Product product; public Producer(Product product) {
this.product = product;
} //生产
@Override
public void run() {
for (int i = 0; i < 10; i++) {
this.product.push("产品" + i);
}
}
} //消费者
class Consumer extends Thread {
Product product; public Consumer(Product product) {
this.product = product;
} //消费
@Override
public void run() {
for (int i = 0; i < 10; i++) {
this.product.pop();
}
}
} //产品
class Product {
String product;
boolean flag = true; //生产
public synchronized void push(String product) {
if (!flag) {
try {
this.wait();
} catch (InterruptedException ignored) { }
}
System.out.println("生产了" + product);
//通知消费
this.notifyAll();
this.product = product;
this.flag = !this.flag; } //消费
public synchronized void pop() {
if (flag) {
try {
this.wait();
} catch (InterruptedException ignored) { }
}
System.out.println("消费了" + this.product);
//通知生产者
this.notifyAll();
this.flag = !this.flag;
}
}

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

线程池

背景

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

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

优点

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

参数说明

  • 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:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

代码演示

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; public class Test {
public static void main(String[] args) {
//创建线程池
ExecutorService service = Executors.newFixedThreadPool(10);
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread()); //关闭连接
service.shutdown();
}
} class MyThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}

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

总结

  • 线程就是独立的执行路径;
  • 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,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. CPU,GPU,GPGPU

    CPU,GPU,GPGPU 1.基本概念 1.1  GPU 图形处理器(bai英语:Graphics Processing Unit,缩写:GPU),又称显示核心.视觉du处理器.zhi显示芯片,是一 ...

  2. NX二次开发-通过3x3矩阵获取XYZ轴矢量

    函数:UF_CSYS_ask_wcs() 函数说明:通过3x3矩阵获取XYZ轴矢量 用法: 1 #include <uf.h> 2 #include <uf_mtx.h> 3 ...

  3. 10:ValueError: Cannot assign "'2'": "Comment.article" must be a "Article" instance

    报错中出现类似ValueError: Cannot assign "'XXX'": "Comment.article" must be a "XXX& ...

  4. 禁止特定IP访问Oracle数据库

    通过使用数据库服务器端的sqlnet.ora文件可以实现禁止指定IP主机访问数据库的功能,这对于提升数据库的安全性有很大的帮助,与此同时,这个技术为我们管理和约束数据库访问控制提供了有效的手段 在sq ...

  5. .NET Core/.NET5/.NET6 开源项目汇总5:权限管理系统项目

    系列目录     [已更新最新开发文章,点击查看详细] 企业管理系统一般包含后台管理UI.组织机构管理.权限管理.日志.数据访问.表单.工作流等常用必备功能.下面收集的几款优秀开源的管理系统,值得大家 ...

  6. Java-学习日记(Java8异步)

    今天用到的中异步操作:异步编程与异步处理数据 //里面返回其他接口服务使用CompletableFuture CompletableFuture.runAsync(()->{ driverNoR ...

  7. golang 写文件--详细解释

    1,不覆盖指定的文件 先看代码怎么写,下面再具体解释. func writeToFile(msg string) { f, err := os.OpenFile("/home/mingbai ...

  8. JUnit5注解学习指引

    注解(Annotations)是JUnit的标志性技术,本文就来对它的20个注解,以及元注解和组合注解进行学习. 20个注解 在org.junit.jupiter.api包中定义了这些注解,它们分别是 ...

  9. gRPC(2):四种基本通信模式

    在 gRPC(1):入门及简单使用(go) 中,我们实现了一个简单的 gRPC 应用程序,其中双方通信是简单的请求-响应模式,没发出一个请求都会得到一个响应,然而,借助 gRPC 可以实现不同的通信模 ...

  10. Kubernetes之deployment

    Kubernetes实现了零停机的升级过程.升级操作可以通过使用ReplicationController或者ReplicaSet实现,但是Kubernetes提供了另一种基于ReplicaSet的资 ...