1.什么是线程安全问题?

从某个线程开始访问到访问结束的整个过程,如果有一个访问对象被其他线程修改,那么对于当前线程而言就发生了线程安全问题;

如果在整个访问过程中,无一对象被其他线程修改,就是线程安全的,即存在两个或者两个以上的线程对象共享同一个资源

2.线程安全问题产生的根本原因

首先是多线程环境,即同时存在有多个操作者,单线程环境不存在线程安全问题。在单线程环境下,任何操作包括修改操作都是操作者自己发出的,

操作者发出操作时不仅有明确的目的,而且意识到操作的影响。

多个操作者(线程)必须操作同一个对象,只有多个操作者同时操作一个对象,行为的影响才能立即传递到其他操作者。

多个操作者(线程)对同一对象的操作必须包含修改操作,共同读取不存在线程安全问题,因为对象不被修改,未发生变化,不能产生影响。

综上可知,线程安全问题产生的根本原因是共享数据存在被并发修改的可能,即一个线程读取时,允许另一个线程修改

3.有线程安全的实例

模拟火车站售票窗口,开启三个窗口售票,总票数为20张

实例一:

package com.practise.threadsafe;

//模拟火车站售票窗口,开启三个窗口售票,总票数为100张
//存在线程的安全问题
class Window extends Thread {
static int ticket = 20; public void run() {
while (true) {
if (ticket > 0) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket--);
} else {
break;
}
}
}
} public class TestWindow {
public static void main(String[] args) {
Window w1 = new Window();
Window w2 = new Window();
Window w3 = new Window(); w1.setName("窗口1");
w2.setName("窗口2");
w3.setName("窗口3"); w1.start();
w2.start();
w3.start(); } }
运行结果的一种:出现重复售票及负数票

窗口3售票,票号为:20
窗口2售票,票号为:18
窗口1售票,票号为:19
窗口1售票,票号为:17
窗口3售票,票号为:16
窗口2售票,票号为:17
窗口1售票,票号为:15
窗口3售票,票号为:14
窗口2售票,票号为:13
窗口2售票,票号为:12
窗口3售票,票号为:11
窗口1售票,票号为:10
窗口3售票,票号为:8
窗口2售票,票号为:9
窗口1售票,票号为:7
窗口1售票,票号为:6
窗口2售票,票号为:6
窗口3售票,票号为:5
窗口1售票,票号为:4
窗口3售票,票号为:4
窗口2售票,票号为:3
窗口2售票,票号为:2
窗口1售票,票号为:2
窗口3售票,票号为:2
窗口2售票,票号为:1
窗口1售票,票号为:-1
窗口3售票,票号为:0

实例二:

package com.practise.threadsafe;

//使用实现Runnable接口的方式,售票
/*
* 此程序存在线程的安全问题
*/ class Window1 implements Runnable {
int ticket = 20; public void run() {
while (true) {
if (ticket > 0) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket--);
} else {
break;
}
}
}
} public class TestWindow1 {
public static void main(String[] args) {
Window1 w = new Window1();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w); t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3"); t1.start();
t2.start();
t3.start();
}
}
运行结果的一种:出现重复售票

窗口2售票,票号为:20
窗口1售票,票号为:20
窗口3售票,票号为:20
窗口1售票,票号为:19
窗口3售票,票号为:18
窗口2售票,票号为:17
窗口2售票,票号为:16
窗口3售票,票号为:14
窗口1售票,票号为:15
窗口1售票,票号为:13
窗口2售票,票号为:12
窗口3售票,票号为:11
窗口2售票,票号为:10
窗口1售票,票号为:10
窗口3售票,票号为:10
窗口1售票,票号为:9
窗口3售票,票号为:7
窗口2售票,票号为:8
窗口2售票,票号为:6
窗口3售票,票号为:4
窗口1售票,票号为:5
窗口3售票,票号为:3
窗口1售票,票号为:3
窗口2售票,票号为:3
窗口1售票,票号为:2
窗口3售票,票号为:0
窗口2售票,票号为:1

4.线程安全解决机制Lock和synchronized

4.1  同步代码块synchronized

package com.practise.threadsafe;

/* 同步代码块
* synchronized(同步监视器){
* //需要被同步的代码块(即为操作共享数据的代码)
* }
* 1.共享数据:多个线程共同操作的同一个数据(变量)
* 2.同步监视器:由一个类的对象来充当。哪个线程获取此监视器,谁就执行大括号里被同步的代码。俗称:锁
* 要求:所有的线程必须共用同一把锁!
* 注:在实现的方式中,考虑同步的话,可以使用this来充当锁。但是在继承的方式中,慎用this!
*/
class WindowFirst implements Runnable {
int ticket = 20;// 共享数据 public void run() {
while (true) {
// this表示当前对象,本题中即为w
synchronized (this) {
if (ticket > 0) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket--);
}
}
}
}
} public class SynchronizedCodeBlock {
public static void main(String[] args) {
WindowFirst w = new WindowFirst();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w); t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3"); t1.start();
t2.start();
t3.start();
}
}
运行结果的一种:

窗口1售票,票号为:20
窗口3售票,票号为:19
窗口3售票,票号为:18
窗口2售票,票号为:17
窗口2售票,票号为:16
窗口2售票,票号为:15
窗口2售票,票号为:14
窗口2售票,票号为:13
窗口2售票,票号为:12
窗口3售票,票号为:11
窗口3售票,票号为:10
窗口1售票,票号为:9
窗口1售票,票号为:8
窗口3售票,票号为:7
窗口2售票,票号为:6
窗口2售票,票号为:5
窗口3售票,票号为:4
窗口3售票,票号为:3
窗口3售票,票号为:2
窗口3售票,票号为:1

4.2  同步方法synchronized

package com.practise.threadsafe;

/*
* 同步方法
* 将操作共享数据的方法声明为synchronized。即此方法为同步方法,能够保证当其中一个线程执行
* 此方法时,其它线程在外等待直至此线程执行完此方法
*/ class WindowSecond implements Runnable {
int ticket = 20;// 共享数据 public void run() {
while (true) {
show();
}
} public synchronized void show() {
if (ticket > 0) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket--);
} }
} public class SynchronizedMethod {
public static void main(String[] args) {
WindowSecond w = new WindowSecond();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w); t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3"); t1.start();
t2.start();
t3.start();
}
}
运行结果的一种:

窗口2售票,票号为:20
窗口1售票,票号为:19
窗口3售票,票号为:18
窗口1售票,票号为:17
窗口2售票,票号为:16
窗口1售票,票号为:15
窗口3售票,票号为:14
窗口1售票,票号为:13
窗口2售票,票号为:12
窗口1售票,票号为:11
窗口3售票,票号为:10
窗口1售票,票号为:9
窗口2售票,票号为:8
窗口1售票,票号为:7
窗口3售票,票号为:6
窗口3售票,票号为:5
窗口1售票,票号为:4
窗口2售票,票号为:3
窗口1售票,票号为:2
窗口3售票,票号为:1

4.3  同步锁Lock

package com.practise.threadsafe;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; class WindowThird implements Runnable {
int ticket = 20;// 共享数据
Lock lock = new ReentrantLock(); public void run() {
while (true) {
lock.lock(); // 获取锁
try {
if (ticket > 0) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket--);
}
} finally {
lock.unlock(); // 释放锁
}
}
}
} public class LockThreadSafety {
public static void main(String[] args) {
WindowThird w = new WindowThird();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w); t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3"); t1.start();
t2.start();
t3.start();
}
}
运行结果的一种:

窗口3售票,票号为:20
窗口3售票,票号为:19
窗口3售票,票号为:18
窗口3售票,票号为:17
窗口3售票,票号为:16
窗口3售票,票号为:15
窗口3售票,票号为:14
窗口3售票,票号为:13
窗口3售票,票号为:12
窗口1售票,票号为:11
窗口2售票,票号为:10
窗口3售票,票号为:9
窗口3售票,票号为:8
窗口3售票,票号为:7
窗口3售票,票号为:6
窗口3售票,票号为:5
窗口3售票,票号为:4
窗口3售票,票号为:3
窗口3售票,票号为:2
窗口1售票,票号为:1

   5.synchronized 的局限性 与 Lock 的优点

如果一个代码块被synchronized关键字修饰,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待直至占有锁的线程释放锁。事实上,占有锁的线程释放锁一般会是以下三种情况之一:

  • 占有锁的线程执行完了该代码块,然后释放对锁的占有;
  • 占有锁线程执行发生异常,此时JVM会让线程自动释放锁;
  • 占有锁线程进入 WAITING 状态从而释放锁,例如在该线程中调用wait()方法等。

synchronized 是Java语言的内置特性,可以轻松实现对临界资源的同步互斥访问。那么,为什么还会出现Lock呢?试考虑以下三种情况:

Case 1 :

在使用synchronized关键字的情形下,假如占有锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,那么其他线程就只能一直等待,别无他法。这会极大影响程序执行效率。因此,就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间 (解决方案:tryLock(long time, TimeUnit unit)) 或者 能够响应中断 (解决方案:lockInterruptibly())),这种情况可以通过 Lock 解决。

Case 2 :

我们知道,当多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作也会发生冲突现象,但是读操作和读操作不会发生冲突现象。但是如果采用synchronized关键字实现同步的话,就会导致一个问题,即当多个线程都只是进行读操作时,也只有一个线程在可以进行读操作,其他线程只能等待锁的释放而无法进行读操作。因此,需要一种机制来使得当多个线程都只是进行读操作时,线程之间不会发生冲突。同样地,Lock也可以解决这种情况 (解决方案:ReentrantReadWriteLock) 。

Case 3 :

我们可以通过Lock得知线程有没有成功获取到锁 (解决方案:ReentrantLock) ,但这个是synchronized无法办到的。

上面提到的三种情形,我们都可以通过Lock来解决,但 synchronized 关键字却无能为力。事实上,Lock 是 java.util.concurrent.locks包 下的接口,Lock 实现提供了比 synchronized 关键字 更广泛的锁操作,它能以更优雅的方式处理线程同步问题。也就是说,Lock提供了比synchronized更多的功能。但是要注意以下几点:

  • 1)synchronized是Java的关键字,因此是Java的内置特性,是基于JVM层面实现的。而Lock是一个Java接口,是基于JDK层面实现的,通过这个接口可以实现同步访问;
  • 2)采用synchronized方式不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而 Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致死锁现象

   6.Lock和synchronized的选择

总结来说,Lock和synchronized有以下几点不同:

  •   1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现
  •   2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁
  •   3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断
  •   4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到
  •   5)Lock可以提高多个线程进行读操作的效率

  在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择

Java多线程-----线程安全及解决机制的更多相关文章

  1. Java多线程5:Synchronized锁机制

    一.前言 在多线程中,有时会出现多个线程对同一个对象的变量进行并发访问的情形,如果不做正确的同步处理,那么产生的后果就是“脏读”,也就是获取到的数据其实是被修改过的. 二.引入Synchronized ...

  2. Java多线程——线程之间的协作

    Java多线程——线程之间的协作 摘要:本文主要学习多线程之间是如何协作的,以及如何使用wait()方法与notify()/notifyAll()方法. 部分内容来自以下博客: https://www ...

  3. Java多线程——线程之间的同步

    Java多线程——线程之间的同步 摘要:本文主要学习多线程之间是如何同步的,如何使用volatile关键字,如何使用synchronized修饰的同步代码块和同步方法解决线程安全问题. 部分内容来自以 ...

  4. java 多线程—— 线程让步

    java 多线程 目录: Java 多线程——基础知识 Java 多线程 —— synchronized关键字 java 多线程——一个定时调度的例子 java 多线程——quartz 定时调度的例子 ...

  5. java 多线程—— 线程等待与唤醒

    java 多线程 目录: Java 多线程——基础知识 Java 多线程 —— synchronized关键字 java 多线程——一个定时调度的例子 java 多线程——quartz 定时调度的例子 ...

  6. Linux 多线程 - 线程异步与同步机制

    Linux 多线程 - 线程异步与同步机制 I. 同步机制 线程间的同步机制主要包括三个: 互斥锁:以排他的方式,防止共享资源被并发访问:互斥锁为二元变量, 状态为0-开锁.1-上锁;开锁必须由上锁的 ...

  7. Java多线程--线程及相关的Java API

    Java多线程--线程及相关的Java API 线程与进程 进程是线程的容器,程序是指令.数据的组织形式,进程是程序的实体. 一个进程中可以容纳若干个线程,线程是轻量级的进程,是程序执行的最小单位.我 ...

  8. Java多线程-线程的同步(同步方法)

    线程的同步是保证多线程安全访问竞争资源的一种手段.线程的同步是Java多线程编程的难点,往往开发者搞不清楚什么是竞争资源.什么时候需要考虑同步,怎么同步等等问题,当然,这些问题没有很明确的答案,但有些 ...

  9. Java多线程——线程的优先级和生命周期

    Java多线程——线程的优先级和生命周期 摘要:本文主要介绍了线程的优先级以及线程有哪些生命周期. 部分内容来自以下博客: https://www.cnblogs.com/sunddenly/p/41 ...

随机推荐

  1. java 网络编程(三)简单的即时通讯(UDP传输)

    发送端: package cn.sasa.netDemo2; import java.io.IOException; import java.net.DatagramPacket; import ja ...

  2. 修改.net core MVC默认端口

    默认端口是5000,更改端口修改launchSettings.json.如图:

  3. CentOS SSH免密登陆

    #环境说明客户机:Mac OS X服务器:CentOS 6.5客户端:OpenSSH,OS X及大多数Linux都内置了OpenSSH.’ssh -v’命令可以查看版本. #大致流程1.在客户机创建一 ...

  4. CSS中position属性介绍(新增sticky)

    position的含义是指定类型,取值类型可以有:static.relative.absolute.fixed.inherit 和 sticky,这里sticky是CSS3新发布的一个属性. 1.po ...

  5. zabbix server源码安装

    一.准备工作 yum -y install net-snmp-devel php-bcmath php-ctype php-xml php-xmlreader php-xmlwriter php-se ...

  6. MongoDB limit 选取 skip跳过 sort排序 方法

    MongoDB  limit 选取 skip跳过 sort排序 在mysql里有order by  MongoDB用sort代替order by > db.user.find() { " ...

  7. python-->(set /dict)交集 差集 并集 补集(功能用来做交差并补的)

    # ### 集合 作用:交集 差集 并集 补集(功能用来做交差并补的) '''特征:自动去重 无序''' #定义一个空集合 setvar = set() #set()强制转换成一个空集合的数据类型 p ...

  8. sap 查看类的修饰属性

  9. spring boot+logback+JdbcTemplate打印sql日志

    项目中使用的JdbcTemplate直接在service中执行sql语句,配置如下: 使用IDEA创建的项目自带 main/resource 中自带logback.xml 配置文件,添加以下日志配置, ...

  10. Notes for Neural Network Methods for Natural Language Processing

    什么是深度学习?   一种机器学习算法,based on [多层][非线性变换]的[神经网络]结构 优点:可以使用 低维 稠密 连续 的向量表示不同粒度的语言单元, 还可以使用循环.卷积.递归等神经网 ...