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实现有返回结果的多线程.其中前两种方式线程执行完后都没 ...
随机推荐
- 用ASP.NET创建网站
ASP.NET提供三种框架来创建web应用:WebForms,ASP.NET MVC和ASP.NET WebPages.这三种框架都是稳定成熟的,你可以用任何一种方式开发一个很棒的web应用.不管你选 ...
- 【2-24】for循环嵌套,跳转语句,异常语句,穷举法、迭代法
For循环嵌套与if嵌套相似,是在for中再套for,其结构如下: For(;;) { For(;;){} }经典题型为打印星星例: Console.Write("请输入一个奇数:" ...
- observe.js 源码 学习笔记
/** * observejs --- By dnt http://kmdjs.github.io/ * Github: https://github.com/kmdjs/observejs * MI ...
- HTML 5入门知识——Drag
HTML 5拖放 拖放的步骤: 1.对象元素的draggable属性设置为true,还需要注意的是a元素和img元素必须指定href. 2.编写与拖放有关的事件处理代码: DataTransfer对象 ...
- ST HW3
7. Use the following method printPrimes() for questions a-f below. /******************************** ...
- HDFS Namenode启动过程
文章作者:luxianghao 文章来源:http://www.cnblogs.com/luxianghao/p/6564032.html 转载请注明,谢谢合作. 免责声明:文章内容仅代表个人观点, ...
- 常见Android面试题及答案(详细整理)
1. 请描述一下Activity 生命周期. 答: 如下图所示.共有七个周期函数,按顺序分别是: onCreate(), onStart(), onRestart(), onResume(), onP ...
- nao安装中文包教程
本文介绍nao离线安装中文包 相官方索取 .demo 文件夹,里面包含很多的例程和中文包 下载winscp和putty(或者xshell),下载过程和安装我就不多说了,免费 把nao开机,连接路由器等 ...
- android国际化
语言的国际化 为了提供不同语言的版本,只需要在res中新建几个values文件夹就行 不过文件夹有自己的命名规则 values-语言代码-r国家或者地区的代码 然后我们只需要将不同语言的string. ...
- MyBatis极速入门开发手册(2017-2-6更新)
前言: 本篇文章对我的学习内容做一个归纳梳理,同时也记录一些学习过程中遇上的问题及注意事项等等,可以帮助新入门的朋友们消除一些误区与盲区,希望能帮上一些正在学习的朋友们.在编写时会引用一些mybati ...