java多线程基本概述(三)——同步方法
非线程安全其实是在多个线程对同一个对象实例的变量进行并发访问的时候发生,产生的后果就是脏读,也就是取到的数据是修改过的。而线程安全就是获得的实例变量的值是经过同步处理的,从而不会出现脏读现象。
什么时候使用同步呢?可以运用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实现有返回结果的多线程.其中前两种方式线程执行完后都没 ...
随机推荐
- 去掉Dedecms幻灯片的标题文字和绿色背景
在操作dedecms站群中的站,因为看到首页的幻灯片Flash显示标题和文字,显得有点突兀,觉得应该去除dedecms 幻灯片默认的文字标题和背景,这样会感觉舒服些,下面教大家如何在模板当中去除ded ...
- 一个可以将 json 字符串 直接绑定到 view 上的Android库
android-data-binding 这是一个可以将 json 字符串 直接绑定到 view 上的库, 不用先将 json 转换为 model 类. 传送门(https://github.com/ ...
- tp框架 :操作数据库
操作数据库,进行增删改数据 一.对数据表添加数据(方法:add()) (1)上一篇已经讲过链接数据库了,继续进行对数据库的操作,还是用控制器文件中的HomeController.class文件 看下数 ...
- ACM 重建二叉树
重建二叉树 时间限制:1000 ms | 内存限制:65535 KB 难度:3 描述 题目很简单,给你一棵二叉树的后序和中序序列,求出它的前序序列(So easy!). 输入 输入有多组数 ...
- wemall app微信商城系统Android之通用通知接口demo
wemall-mobile是基于WeMall的Android app商城,只需要在原商城目录下上传接口文件即可完成服务端的配置,客户端可定制修改.本文分享Native(原生)支付模式一demo,供技术 ...
- Xamarin自定义布局系列——瀑布流布局
Xamarin.Forms以Xamarin.Android和Xamarin.iOS等为基础,自己实现了一整套比较完整的UI框架,包含了绝大多数常用的控件,如下图 虽然XF(Xamarin.Forms简 ...
- GCC命令
一. 常用编译命令选项 源程序test.c 1. 无选项编译链接用法:#gcc test.c作用:将test.c预处理.汇编.编译并链接形成可执行文件.这里未指定输出文件,默认输出为a.out. 2. ...
- 路径正确下,Eclipse读取txt文件仍失败
症状:使用Eclipse读取文件时,路径输入确认正确(前提!!!),但控制台总报错: 错误类型一: Exception in thread "main" java.io.FileN ...
- 微信JS分享功能--微信JS系列文章(二)
概述 在上一篇文章微信JS初始化-- 微信JS系列文章(一)中已经介绍了微信JS初始化的相关工作,接下来本文继续就微信JS的分享功能进行描述,供大家参考. 代码 $(document).ready(f ...
- iOS开发之左右抖动效果
CAKeyframeAnimation *shakeAnim = [CAKeyframeAnimation animation]; shakeAnim.keyPath = @"transfo ...