线程同步

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

为什么要创建多线程?

在一般情况下,创建一个线程是不能提高程序的执行效率的,所以要创建多个线程。

  • 为什么要线程同步

    • 多个线程同时运行的时候可能调用线程函数,在多个线程同时对同一个内存地址进行写入,由于CPU时间调度上的问题,写入数据会被多次的覆盖,所以就要使线程同步。

    • 例如:我们去银行存钱,那肯定是我们银行卡里原本的钱加上要存入的钱。但是在你存钱的同时你的朋友在给你的银行卡转钱,这是两个线程,这两个线程同时拿到了银行卡的本金,那么这两个线程最后都会返回一个总金额,那这两个总金额都是不正确的,只有这两次交易有一个先后顺序才行,这就是线程同步的一个原因。

线程同步是意思

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

    • 错误理解:“同”字从字面上容易理解为一起动作,其实不是,“同”字应是指协同、协助、互相配合。
    • 正确理解: 所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回,同时其它线程也不能调用这个方法。按照这个定义,其实绝大多数函数都是同步调用。但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。
    • 在多线程编程里面,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性。

线程同步作用

  • 线程有可能和其他线程共享一些资源,比如,内存,文件,数据库等。
  • 当多个线程同时读写同一份共享资源的时候,可能会引起冲突。这时候,我们需要引入线程“同步”机制,即各位线程之间要有个先来后到,不能一窝蜂挤上去抢作一团。
  • 线程同步的真实意思和字面意思恰好相反。线程同步的真实意思,其实是“排队”:几个线程之间要排队,一个一个对共享资源进行操作,而不是同时进行操作

基本上所有解决线程安全问题的方式都是采用“序列化临界资源访问”的方式,即在同一时刻只有一个线程操作临界资源,操作完了才能让其他线程进行操作,也称作同步互斥访问。

在Java中一般采用synchronized和Lock来实现同步互斥访问。

常用方法

Synchronized关键字

首先我们先来了解一下互斥锁,

互斥锁:就是能达到互斥访问目的的锁。

如果对一个变量加上互斥锁,那么在同一时刻,该变量只能有一个线程能访问,即当一个线程访问临界资源时,其他线程只能等待。

在Java中,每一个对象都有一个锁标记(monitor),也被称为监视器,当多个线程访问对象时,只有获取了对象的锁才能访问。

在我们编写代码的时候,可以使用synchronized修饰对象的方法或者代码块,当某个线程访问这个对象synchronized方法或者代码块时,就获取到了这个对象的锁,这个时候其他对象是不能访问的,只能等待获取到锁的这个线程执行完该方法或者代码块之后,才能执行该对象的方法。

synchronized添加到代码块位置

我们先写一个不加Synchronized的多线程代码,这段代码是创建两个线程,这段代码是创建两个线程分别输出五个数。

多次运行可以发现结果每次不一样,这就导致了不确定性。我们给他加上同步方法会发现一个输出完之后另一个才会输出,这就可以空值共享资源不能同时被两个线程得到。

package hello;

public class Hello {

    public static void main(String[] args) throws Exception {
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
Thread thread1 = new Thread(myThread);
thread.start();
thread1.start();
}
} class OutputData{
//定义输出方法
public void output(Thread thread){
for (int i=0;i<5;i++){
System.out.println(thread.getName()+":"+"输出"+i);
}
}
} class MyThread implements Runnable{
OutputData inserData = new OutputData(); public void run(){
inserData.output(Thread.currentThread());
}
}
直接添加到方法上

我们在OutputData类里面加入synchronized之后在运行就可以看到结果每次都是0123401234

class OutputData{
//定义输出方法
public synchronized void output(Thread thread){
for (int i=0;i<5;i++){
System.out.println(thread.getName()+":"+"输出"+i);
}
}
}
添加在代码块

其实上面的代码还可以这样加,这个里面和上面的原理是一样的。

class OutputData{
//定义输出方法
public void output(Thread thread){
//this就是当前对象
synchronized (this) {
for (int i = 0; i < 5; i++) {
System.out.println(thread.getName() + ":" + "输出" + i);
}
}
}
}

释放锁时机

如果一个方法或者代码块被synchronized关键字修饰,当线程获取到该方法或代码块的锁,其他线程是不能继续访问该方法或代码块的。

而其他线程要能访问该方法或代码块,就必须要等待获取到锁的线程释放这个锁,而在这里释放锁只有两种情况:

  • 线程执行完代码块,自动释放锁;
  • 程序报错,jvm让线程自动释放锁。

Lock锁同步

上面我们已经说了synchronized锁释放的时机

但是可能会有一种情况,当一个线程获取到对象的锁,然后在执行过程中因为一些原因(等待IO,调用sleep方法)被阻塞了,这个时候锁还在被阻塞的线程手中,而其他线程这个时候除了等之外,没有任何办法,我们想一想这样子会有多影响程序的效率。因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。

在比如,当多个线程操作同一个文件的时候,同时读写是会冲突的,同时写也是会冲突的,但是同时读是不会发生冲突的,而我们如果用synchronized来实现同步,就会出现一个问题:

如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。

因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,而通过Lock就可以办到。

总的来说Lock要比synchronized提供的功能更多,可定制化的程度也更高,Lock不是Java语言内置的,而是一个类。

方法解释

先看一下Lock接口的方法

  • lock()、tryLock()和lockInterruptibly()方法是用来获取锁的
  • unlock()方法是用来释放锁的。
  • tryLock()顾名思义,是用来尝试获取锁的,并且该方法有返回值,表示获取成功与否,获取成功返回true,失败返回false,从方法可以发现,该方法如果没有获取到锁时不会继续等待的,而是会直接返回值。
  • tryLock()的重载方法tryLock(long time, TimeUnit unit)功能类似,只是这个方法会等待一段时间获取锁,如果过了等待时间还未获取到锁就会返回false,如果在等待时间之内拿到锁则返回true。

首先lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。

由于在前面讲到如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。

一般格式:

        Lock lock = ...;
lock.lock();
try {
//处理任务
}catch (Exception e){
//捕捉异常
}finally{
lock.unlock();
}

使用Lock.lock()获取锁

package hello;

import javax.sound.sampled.FloatControl;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; public class Hello { public static void main(String[] args) {
Lock lock = new ReentrantLock(); OutputData outputData = new OutputData();
new Thread(){
public void run(){
outputData.output(Thread.currentThread(),lock);
};
}.start(); new Thread(){
public void run(){
outputData.output(Thread.currentThread(),lock);
};
}.start(); }
} class OutputData{
public void output(Thread thread,Lock lock){
lock.lock();
try {
System.out.println(thread.getName()+"得到了锁");
Thread.sleep(100);//加这个睡眠是为了结果效果明显
}catch (Exception e){
e.printStackTrace();
}finally {
System.out.println(thread.getName()+"释放了锁");
lock.unlock();
} }
}

使用tryLock()获取锁

把OutPutData类里面的output方法修改一下就可以了

class OutputData{
public void output(Thread thread,Lock lock){
if(lock.tryLock()){
try {
System.out.println(thread.getName()+"得到了锁");
Thread.sleep(100);//加这个睡眠是为了结果效果明显
}catch (Exception e){
e.printStackTrace();
}finally {
System.out.println(thread.getName()+"释放了锁");
lock.unlock();
}
}else{
System.out.println(thread.getName()+"获取锁失败");
} }
}

volatile同步实现

volatile含义和特点

我们知道每个线程运行的时候都有自己的工作内存,会把变量拷贝到自己的缓存中去,一般情况下你在自己缓存修改的变量不会立即重新写入主内存,这就导致类多线程同步问题,那么volatile关键字的特点是:

  • 当一个共享变量被volatile修饰时,它就具备了“可见性”,即这个变量被一个线程修改时,这个改变会立即被其他线程知道。就是你在这个线程修改了这个变量,另外的线程会立刻知道,也改变。
  • 当一个共享变量被volatile修饰时,会禁止“指令重排序”

volatile关键字会产生什么效果

  • 使用volatile关键字会强制将变量的修改的值立即写至主内存;
  • 使用volatile关键字,当线程对某个变量(这个变量定义为V1)修改时,会强制将所有用到变量V1的线程对应的缓存中V1的缓存行置为无效。
  • 由于线程的V1缓存行无效,所以在运行时线程会读取主存中V1变量的值。
  • 所以到最后线程读取到的就是V1最新的值

volatile应用示例

这段代码可以看出我们并没给有加锁,但是这个int变量sum还是按顺序加的,说明他在改变之后立即就把主内存里的变量改变了。也算是一种同步方式吧。

package hello;

public class Hello {

    public static void main(String[] args) {
AddClass addClass = new AddClass();
new Thread() {
public void run(){
for (int j=0;j<100;j++){
addClass.add();
System.out.println(Thread.currentThread().getName()+":"+addClass.sum);
}
}
}.start();
new Thread(){
public void run(){
for (int j=0;j<100;j++){
addClass.add();
System.out.println(Thread.currentThread().getName()+":"+addClass.sum);
}
}
}.start();
}
} class AddClass{
public volatile int sum = 0;
public void add(){
sum++;
}
}

多线程同步小应用

火车站买票

package hello;

public class Hello {

    public static void main(String[] args) {
//实例化站台对象,
Station station1=new Station();
Station station2=new Station();
Station station3=new Station(); // 让每一个站台对象各自开始工作
station1.start();
station2.start();
station3.start();
}
} class Station extends Thread {
static volatile int p = 20;
static Object ob = new Object();
public void run() {
while (p > 0) {
synchronized (ob) {
if (p > 0) {
System.out.println("卖出了第" + p + "张票"); p = p - 1;
}
}
synchronized (this) {
if (p == 0) {
System.out.println("票卖完了");
}
}
try {
Thread.sleep(100);
} catch (Exception e) { }
} }
}

Java多线程之线程同步【synchronized、Lock、volatitle】的更多相关文章

  1. java多线程:线程同步synchronized(不同步的问题、队列与锁),死锁的产生和解决

    0.不同步的问题 并发的线程不安全问题: 多个线程同时操作同一个对象,如果控制不好,就会产生问题,叫做线程不安全. 我们来看三个比较经典的案例来说明线程不安全的问题. 0.1 订票问题 例如前面说过的 ...

  2. 关于Java多线程的线程同步和线程通信的一些小问题(顺便分享几篇高质量的博文)

    Java多线程的线程同步和线程通信的一些小问题(顺便分享几篇质量高的博文) 前言:在学习多线程时,遇到了一些问题,这里我将这些问题都分享出来,同时也分享了几篇其他博客主的博客,并且将我个人的理解也分享 ...

  3. Java多线程 3 线程同步

    在之前,已经学习到了线程的创建和状态控制,但是每个线程之间几乎都没有什么太大的联系.可是有的时候,可能存在多个线程多同一个数据进行操作,这样,可能就会引用各种奇怪的问题.现在就来学习多线程对数据访问的 ...

  4. Java多线程与线程同步

    六.多线程,线程,同步 ①概念: 并行:指两个或多个在时间同一时刻发生(同时发生) 并发:指两个或多个事件在同一时间段内发生 具体概念: 在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多 ...

  5. Java多线程 | 02 | 线程同步机制

    同步机制简介 ​ 线程同步机制是一套用于协调线程之间的数据访问的机制.该机制可以保障线程安全.Java平台提供的线程同步机制包括: 锁,volatile关键字,final关键字,static关键字,以 ...

  6. java ->多线程_线程同步、死锁、等待唤醒机制

    线程安全 如果有多个线程在同时运行,而这些线程可能会同时运行这段代码.程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的. l  我们通过一个案例,演示线 ...

  7. java 多线程 day03 线程同步

    package com.czbk.thread; /** * Created by chengtao on 17/12/3. 线程安全问题: 线程安全出现 的根本原因: 1. 存在两个或者两个以上 的 ...

  8. 【多线程】线程同步 synchronized

    由于同一进程的多个线程共享同一块存储空间 , 在带来方便的同时,也带来了访问 冲突问题 , 为了保证数据在方法中被访问时的正确性 , 在访问时加入 锁机制synchronized , 当一个线程获得对 ...

  9. java 多线程:线程安全问题synchronized关键字解决

    背景: 多个线程同时修改一个变量时,有概率导致两次修改其中某些次被覆盖. 例如:如下案例一个变量值为3,三个线程同时对其-1,如果按顺序执行,每次减完的结果应该是2,1,0.但实际运行中有可能变为0, ...

随机推荐

  1. js reduce累加器

    ​ reduce 是es6 新增的数组操作方法 意为累加器 使用方法如下 [1,1,1,1].reduce((total,currentValue,index)=>{ },initialValu ...

  2. pytest(7)-yield与终结函数

    通过上一篇文章,我们已经知道了pytest中,可以使用Fixture来完成运行测试用例之前的一些操作如连接数据库,以及测试执行之后自动去做一些善后工作如清空脏数据.关闭数据库连接等. 我们已经学会了f ...

  3. Solution -「Gym 102759I」Query On A Tree 17

    \(\mathcal{Description}\)   Link.   给定一棵含 \(n\) 个结点的树,结点 \(1\) 为根,点 \(u\) 初始有点权 \(a_u=0\),维护 \(q\) 次 ...

  4. 【流行前沿】联邦学习 Federated Learning with Only Positive Labels

    核心问题:如果每个用户只有一类数据,如何进行联邦学习? Felix X. Yu, , Ankit Singh Rawat, Aditya Krishna Menon, and Sanjiv Kumar ...

  5. 6.Flink实时项目之业务数据分流

    在上一篇文章中,我们已经获取到了业务数据的输出流,分别是dim层维度数据的输出流,及dwd层事实数据的输出流,接下来我们要做的就是把这些输出流分别再流向对应的数据介质中,dim层流向hbase中,dw ...

  6. LEETCODE 之写在前面

    不知道能坚持多久,甚至不知道能不能坚持下去. 不知道是先看刷题的笔记好 ,还是直接刷题遇到再说好. 不知道是随机刷的好,还是从头向后这样刷好. 反正,勇敢昌兄,不怕困难.

  7. 想找好用的BI软件?看这一篇就够了:2021年好用的BI软件推荐

    很多厂商活跃在商业智能(下面称BI)领域.事实上,能够满足用户需要的BI产品和方案必须建立在稳定.整合的平台之上,该平台需要提供用户管理.安全性控制.连接数据源以及访问.分析和共享信息的功能.那么,有 ...

  8. 【C# 异常处理】StackTrace 堆栈跟踪

    作用 在使用.NET编写的代码在debug时很容易进行排查和定位问题,一旦项目上线并出现问题的话那么只能依靠系统日志来进行问题排查和定位,但当项目复杂时,即各种方法间相互调用将导致要获取具体的出错方法 ...

  9. iOS动态库和静态库的运用

    概念认识 什么是库 库是共享程序代码的方式,库从本质上来说是一种可执行代码的二进制格式,可以被载入内存中执行.在开发过程中,一些核心技术或者常用框架,出于安全性和稳定性的考虑,不想被外界知道,所以会把 ...

  10. 教程1--安装Git软件

    在https://git-scm.com/下载git for windows,双击安装即可. (1)单击Next (2)选择安装目录 (3)勾选创建桌面快捷方式.Git Bash.Git GUi.已经 ...