非线程安全其实是在多个线程对同一个对象实例的变量进行并发访问的时候发生,产生的后果就是脏读,也就是取到的数据是修改过的。而线程安全就是获得的实例变量的值是经过同步处理的,从而不会出现脏读现象。
什么时候使用同步呢?可以运用Brian的同步规则:
如果你正在写一个变量,它可能接下来将被另一个线程读取,或者正在读取一个上一次已经被另一个线程写过的变量,那么你就必须使用同步,而且,读写线程都必须使用相同发的监视器锁同步。
1.1、同步语句
1.1.1、实例变量非线程安全
如果我们把多个线程并发访问的实例变量转化成方法里面的局部变量,那么就不会产生线程不安全的情况了。因为每个线程拿到的变量都是该线程自己拥有,类似于ThreadLocal类的思想。下面这个例子将变量变为局部变量从而实现线程安全。
package soarhu;
import java.util.concurrent.TimeUnit;
class ObjectMonitor{
void add(String name){
try {
int num = 0; //方法里面的局部变量,不会出现并发竞争的情况。
if(name.equals("a")){
num = 100;
System.out.println("a set finish");
TimeUnit.MILLISECONDS.sleep(2);
}else{
num = 200;
System.out.println("b set finish");
}
System.out.println("user: "+name+" num: "+num);
}catch (InterruptedException e){
e.printStackTrace();
}
}
} class ThreadA extends Thread{
private ObjectMonitor monitor ; ThreadA(ObjectMonitor monitor) {
super();
this.monitor = monitor;
} @Override
public void run() {
monitor.add("a");
}
} class ThreadB extends Thread{
private ObjectMonitor monitor ; ThreadB(ObjectMonitor monitor) {
super();
this.monitor = monitor;
} @Override
public void run() {
monitor.add("B");
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
ObjectMonitor objectMonitor = new ObjectMonitor();
ThreadA threadA = new ThreadA(objectMonitor);
ThreadB threadB = new ThreadB(objectMonitor);
threadA.start();
threadB.start();
}
}
输出结果:
a set finish
b set finish
user: B num: 200
user: a num: 100
如果将第6行的代码移到方法外面,则会出现线程安全的问题,改完后,运行结果:
a set finish
b set finish
user: B num: 200
user: a num: 200
因为此时add()方法访问的变量num为类成员变量,而且add方法没有进行同步,那么a,b两个线程就会同时进入add()方法对num变量进行修改。当a线程把num设置成100后。执行打印语句之前,这时候b线程进入了add()方法,设置num的值为200,那么最终就出现了脏读。解决方法是在add()方法上加入synchronized。使其成为同步方法。这样就会同步访问该方法,从而避免线程不安全的问题了。加入后程序的执行结果为:
a set finish
user: a num: 100
b set finish
user: B num: 200
1.1.2、多个对象多个锁
稍微修改一下上面方法,使其a,b两个线程拥有不同的锁对象,那么add()同步方法的同步意义对现在a,b两个线程没有意义了。因为锁对象不同。
package soarhu;
import java.util.concurrent.TimeUnit;
class ObjectMonitor{
int num = 0;
synchronized void add(String name){
try {
if(name.equals("a")){
num = 100;
System.out.println("a set finish");
TimeUnit.MILLISECONDS.sleep(2);
}else{
num = 200;
System.out.println("b set finish");
}
System.out.println("user: "+name+" num: "+num);
}catch (InterruptedException e){
e.printStackTrace();
}
}
} class ThreadA extends Thread{
private ObjectMonitor monitor ; ThreadA(ObjectMonitor monitor) {
super();
this.monitor = monitor;
} @Override
public void run() {
monitor.add("a");
}
} class ThreadB extends Thread{
private ObjectMonitor monitor ; ThreadB(ObjectMonitor monitor) {
super();
this.monitor = monitor;
} @Override
public void run() {
monitor.add("B");
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
ObjectMonitor objectMonitor = new ObjectMonitor();
ObjectMonitor objectMonitor2 = new ObjectMonitor();
ThreadA threadA = new ThreadA(objectMonitor);
ThreadB threadB = new ThreadB(objectMonitor2);//使用不同的同步对象
threadA.start();
threadB.start();
}
}
输出结果:
a set finish
b set finish
user: B num: 200
user: a num: 100
结果正确,且执行方式不是同步方式,而是异步方式。那么如何使这个方式仍然按照同步的方式进行访问呢?很简单,用同步代码块
修改部分代码,如下:
class ObjectMonitor{
int num = 0;
void add(String name){
synchronized (Integer.TYPE) { //这里使用同步代码块,这里的监视器jvm里只有一份,故可以起到监视器同步的作用
try {
if (name.equals("a")) {
num = 100;
System.out.println("a set finish");
TimeUnit.MILLISECONDS.sleep(2);
} else {
num = 200;
System.out.println("b set finish");
}
System.out.println("user: " + name + " num: " + num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
1.1.3、synchronized方法与锁对象
有上面的代码可以知道,synchronized方法的锁监视器对象就是该方法所属的对象。下面看看对多个方法的调用。
package soarhu;
import java.util.concurrent.TimeUnit;
class ObjectMonitor{
synchronized void a(String name){
try {
System.out.println("a method start: "+Thread.currentThread().getName()+" begin time "+System.currentTimeMillis());
TimeUnit.MILLISECONDS.sleep(5);
System.out.println("a end time: " + System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
} void b(String name){
try {
System.out.println("b method start: "+Thread.currentThread().getName()+" begin time "+System.currentTimeMillis());
TimeUnit.MILLISECONDS.sleep(5);
System.out.println("b end time: " + System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} class ThreadA extends Thread{
private ObjectMonitor monitor ; ThreadA(ObjectMonitor monitor) {
super();
this.monitor = monitor;
} @Override
public void run() {
monitor.a("a");
}
} class ThreadB extends Thread{
private ObjectMonitor monitor ; ThreadB(ObjectMonitor monitor) {
super();
this.monitor = monitor;
} @Override
public void run() {
monitor.b("B");
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
ObjectMonitor objectMonitor = new ObjectMonitor();
ThreadA threadA = new ThreadA(objectMonitor);
ThreadB threadB = new ThreadB(objectMonitor);
threadA.start();
threadB.start();
}
}
输出结果:
a method start: Thread-0 begin time 1492413285921
b method start: Thread-1 begin time 1492413285921
b end time: 1492413285927
a end time: 1492413285927
可以看到a,b两个线程同步访问。虽然a线程拥有objectMonitor的锁,但是b方法并没有加同步块,所以b线程任然可以访问该监视器对象的其他非同步方法。
在b()方法加上同步关键字后,
synchronized void b(String name){
try {
System.out.println("b method start: "+Thread.currentThread().getName()+" begin time "+System.currentTimeMillis());
TimeUnit.MILLISECONDS.sleep(5);
System.out.println("b end time: " + System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
输出结果:
a method start: Thread-0 begin time 1492413504482
a end time: 1492413504489
b method start: Thread-1 begin time 1492413504489
b end time: 1492413504494
已经同步了,那么我们可以得到结论:
A线程现持有监视器对象的锁,B线程可以异步的调用该监视器对象中的非同步方法,如果B线程需要调用该监视器对象的同步方法则需要等待A线程释放锁。
1.1.4、脏读
虽然通过同步代码块对赋值进行同步,但在取值的时候,如果不加同步代码块也可能会出现脏读的情况,实例代码如下:
package soarhu;
import java.util.concurrent.TimeUnit;
class ObjectMonitor{
private String username ="c";
private String password ="ccc";
synchronized void set(String username,String password){
try {
this.username=username;
TimeUnit.MILLISECONDS.sleep(5);
this.password = password;
System.out.println("SET->thread-name: "+Thread.currentThread().getName()+" username: "+username+" password: "+password);
} catch (InterruptedException e) {
e.printStackTrace();
}
} void get(){
System.out.println("GET->thread-name: "+Thread.currentThread().getName()+" username: "+username+" password: "+password);
}
} class ThreadA extends Thread{
private ObjectMonitor monitor ; ThreadA(ObjectMonitor monitor) {
super();
this.monitor = monitor;
} @Override
public void run() {
monitor.set("a","aaa");
}
} public class Test {
public static void main(String[] args) throws InterruptedException {
ObjectMonitor objectMonitor = new ObjectMonitor();
ThreadA threadA = new ThreadA(objectMonitor);
threadA.start();
TimeUnit.MILLISECONDS.sleep(2);
objectMonitor.get();
}
}
输出结果:
GET->thread-name: main username: a password: ccc
SET->thread-name: Thread-0 username: a password: aaa
虽然set()方法是同步方法,但是get()方法不是同步方法,main线程读取值的过程中会读到中间状态的值(易变性)。解决办法是在get()方法加上同步代码块即可。
synchronized void get(){
System.out.println("thread-name: "+Thread.currentThread().getName()+" username: "+username+" password: "+password);
}
输出结果:
SET->thread-name: Thread-0 username: a password: aaa
GET->thread-name: main username: a password: aaa
此时已经线程安全,不会出现脏读的情况了。set,get方法同步进行。可以理解为ACID里面的脏读,a线程修改了变量没有提交,但是b线程现在读到了未提交的变量。即为脏读。
1.1.4、锁重入
synchronized拥有锁重入的功能,也就是在使用此关键字时,当一个线程得到一个对象锁后,再次请求此对象的锁是可以再次得到该对象的锁的,这也证明了在一个synchronized方法/块的内部调用该类其他的synchronized方法/块时,是永远可以得到锁的。
package soarhu;
class ObjectMonitor{ synchronized void set1(){
System.out.println("set1");
set2();
} synchronized void set2(){
System.out.println("set2");
set3();
} synchronized void set3(){
System.out.println("set3");
}
} class ThreadA extends Thread{
private ObjectMonitor monitor ; ThreadA(ObjectMonitor monitor) {
super();
this.monitor = monitor;
} @Override
public void run() {
monitor.set1();
}
} public class Test {
public static void main(String[] args) throws InterruptedException {
ObjectMonitor objectMonitor = new ObjectMonitor();
ThreadA threadA = new ThreadA(objectMonitor);
threadA.start();
}
}
输出结果:
set1
set2
set3
可重入锁的概念就是自己可以获取自己的内部锁。当set1()没有释放锁的情况下调用set2().而且能调用通,说明这个锁是可以重入的。可重入锁也支持在父子类继承环境中。
package soarhu;
class ObjectMonitor{
synchronized void set1(){
System.out.println("set1()");
} }
class ObjectMonitorSub extends ObjectMonitor{
@Override
synchronized void set1() {
super.set1();
System.out.println("set2()");
}
}
class ThreadA extends Thread{
private ObjectMonitor monitor ; ThreadA(ObjectMonitor monitor) {
super();
this.monitor = monitor;
} @Override
public void run() {
monitor.set1();
}
} public class Test {
public static void main(String[] args) throws InterruptedException {
ObjectMonitor objectMonitor = new ObjectMonitorSub();
ThreadA threadA = new ThreadA(objectMonitor);
threadA.start();
}
}
输出结果:
set1()
set2()
可以知道,子类可以重入父类的锁。
1.1.5、出现异常时,锁会被释放
package soarhu;
class ObjectMonitor{
synchronized void set1(){
if (Thread.currentThread().getName().equals("a")){
System.out.println("threadName: "+Thread.currentThread().getName()+" run time: "+ System.currentTimeMillis());
while (true){
try {
Thread.sleep(3000);
throw new RuntimeException("hello kitty");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}else{
System.out.println("thread b run time: "+ System.currentTimeMillis());
}
} } class ThreadA extends Thread{
private ObjectMonitor monitor ; ThreadA(ObjectMonitor monitor) {
super();
this.monitor = monitor;
} @Override
public void run() {
monitor.set1();
}
}
class ThreadB extends Thread{
private ObjectMonitor monitor ; ThreadB(ObjectMonitor monitor) {
super();
this.monitor = monitor;
} @Override
public void run() {
monitor.set1();
}
} public class Test {
public static void main(String[] args) throws InterruptedException {
ObjectMonitor objectMonitor = new ObjectMonitor();
ThreadA threada = new ThreadA(objectMonitor);
ThreadB threadb = new ThreadB(objectMonitor);
threada.setName("a");
threadb.setName("b");
threada.start();
Thread.sleep(500);
threadb.start();
}
}
输出结果:
threadName: a run time: 1492416360445
thread b run time: 1492416363445
Exception in thread "a" java.lang.RuntimeException: hello kitty
at soarhu.ObjectMonitor.set1(Test.java:9)
at soarhu.ThreadA.run(Test.java:31)
a线程运行3秒后,抛出异常,此时b线程若获得锁则会执行b的那行打印语句,如果不释放锁,则不会有这行输出。根据时间差可以看到差了3000毫秒,正好是a线程的执行时间。a异常一旦抛出,b线程就有执行的机会了。
java多线程基本概述(三)——同步方法的更多相关文章
-
java多线程中的三种特性
java多线程中的三种特性 原子性(Atomicity) 原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行. 如果一个操作时原子性的,那么多线程并 ...
-
“全栈2019”Java多线程第二十章:同步方法产生死锁的例子
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...
-
Java多线程学习(三)volatile关键字
转载请备注地址:https://blog.csdn.net/qq_34337272/article/details/79680693 系列文章传送门: Java多线程学习(一)Java多线程入门 Ja ...
-
如何实现有返回值的多线程 JAVA多线程实现的三种方式
可返回值的任务必须实现Callable接口,类似的,无返回值的任务必须Runnable接口.执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable ...
-
Java 多线程基础(三) start() 和 run()
Java 多线程基础(三) start() 和 run() 通过之前的学习可以看到,创建多线程过程中,最常用的便是 Thread 类中的 start() 方法和线程类的 run() 方法.两个方法都包 ...
-
Java多线程——<;一>;概述、定义任务
一.概述 为什么使用线程?从c开始,任何一门高级语言的默认执行顺序是“按照编写的代码的顺序执行”,日常开发过程中写的业务逻辑,但凡不涉及并发的,都是让一个任务顺序执行以确保得到想要的结果.但是,当你的 ...
-
[转载] java多线程总结(三)
转载自: http://www.cnblogs.com/lwbqqyumidi/p/3821389.html 作者:Windstep 本文主要接着前面多线程的两篇文章总结Java多线程中的线程安全问题 ...
-
Java多线程编程核心技术(三)多线程通信
线程是操作系统中独立的个体,但这些个体如果不经过特殊的处理就不能成为一个整体.线程间的通信就是成为整体的必用方案之一,可以说,使线程间进行通信后,系统之间的交互性会更强大,在大大提高CPU利用率的同时 ...
-
Java多线程-线程的同步(同步方法)
线程的同步是保证多线程安全访问竞争资源的一种手段.线程的同步是Java多线程编程的难点,往往开发者搞不清楚什么是竞争资源.什么时候需要考虑同步,怎么同步等等问题,当然,这些问题没有很明确的答案,但有些 ...
-
JAVA多线程实现的三种方式
JAVA多线程实现方式主要有三种:继承Thread类.实现Runnable接口.使用ExecutorService.Callable.Future实现有返回结果的多线程.其中前两种方式线程执行完后都没 ...
随机推荐
-
bzoj3316: JC loves Mkk
Description Input 第1行,包含三个整数.n,L,R.第2行n个数,代表a[1..n]. Output 仅1行,表示询问答案.如果答案是整数,就输出整数:否则,输出既约分数“P/Q”来 ...
-
u3d 性能优化
http://blog.csdn.net/candycat1992/article/details/42127811 写在前面 这一篇是在Digital Tutors的一个系列教程的基础上总结扩展而得 ...
-
字符串&;数组的相互转换
字符串 -> 数组 方法一: $str = "abcd" $s2 = $str.GetEnumerator() #$s2是无法使用下标的方式进行索引的,因为其不是array ...
-
Android 拉伸四周";空白";,中间内容不变的9Patch
系统自带tools下的9Patch制作工具: 在左边,画线的视图里,Zoom到最大,分别在上方和左方的四个端点,各点一下,效果: 看看这四个点的位置: 二.补充一个箭头尾巴可*拉伸的9patch示例 ...
-
利用URLScan工具过滤URL中的特殊字符(仅针对IIS6)-- 解决IIS短文件名漏洞
IIS短文件名漏洞在windows服务器上面非常常见,也就是利用“~”字符猜解暴露短文件/文件夹名,比如,采用这种方式构造URL:http://aaa.com/abc~1/.aspx,根据IIS返回的 ...
-
阿里聚安全攻防挑战赛第三题Android PwnMe解题思路
阿里聚安全攻防挑战赛第三题Android PwnMe解题思路 大家在聚安全挑战赛正式赛第三题中,遇到android app 远程控制的题目.我们今天带你一探究竟,如何攻破这道题目. 一.题目 购物应用 ...
-
HDU 1157 Who&#39;s in the Middle
#include <cstdio> #include <algorithm> using namespace std; int main() { int n; while(sc ...
-
Struts2 设置global timer
设置全局的timer需要在web.xml中添加servlet, 并设置load-on-startup 为 1, 然后在servlet的init()中开启timer, 具体代码如下: 1. web.xm ...
-
Golang 网络爬虫框架gocolly/colly 三
Golang 网络爬虫框架gocolly/colly 三 熟悉了<Golang 网络爬虫框架gocolly/colly一>和<Golang 网络爬虫框架gocolly/colly二& ...
-
使用pyinstaller打包多个py文件为一个EXE文件
1. 安装pyinstaller. pip install pyinstaller !!!!64位win7上打包后始终不能用,提示找不到ldap模块,换了32位win7就好了.!!!!(代码中涉及ld ...