版权声明:本文出自汪磊的博客,转载请务必注明出处。

Java线程系列文章只是自己知识的总结梳理,都是最基础的玩意,已经掌握熟练的可以绕过。

一、一个简单的Demo引发的血案

关于线程同步问题我们从一个简单的Demo现象说起。Demo特别简单就是开启两个线程打印字符串信息。

OutPutStr类源码:

 public class OutPutStr {

     public void out(String str) {
for (int i = 0; i < str.length(); i++) {
System.out.print(str.charAt(i));
}
System.out.println();
}
}

很简单吧,就是一个方法供外界调用,调用的时候传进来一个字符串,方法逐个取出字符串的字符并打印到控制台。

接下来,我们看main方法中逻辑:

 public static void main(String[] args) {
//
final OutPutStr o = new OutPutStr();
new Thread(new Runnable() { @Override
public void run() {
//
while(true){
o.out("111111111111");
}
}
}).start();
new Thread(new Runnable() { @Override
public void run() {
//
while(true){
o.out("222222222222");
}
}
}).start();
}

也很简单,就是开启两个线程分别调用OutPutStr中out方法不停打印字符串信息,运行程序打印信息如下:

 222222222222
222222222222
22222222222111111111
2
111111111111
111111111111
1111222222222211111111
111111111111

咦?和我们想的不一样啊,怎么还会打印出22222222222111111111这样子的信息,这是怎么回事呢?

二、原因解析

我们知道线程的执行是CPU随机调度的,比如我们开启10个线程,这10个线程并不是同时执行的,而是CPU快速的在这10个线程之间切换执行,由于切换速度极快使我们感觉同时执行罢了。发生上面问题的本质就是CPU对线程执行的随机调度,比如A线程此时正在打印信息还没打印完毕此时CPU切换到B线程执行了,B线程执行完了又切换回A线程执行就会导致上面现象发生。

线程同步问题往往发生在多个线程调用同一方法或者操作同一变量,但是我们要知道其本质就是CPU对线程的随机调度,CPU无法保证一个线程执行完其逻辑才去调用另一个线程执行。

三、同步方法解决上述问题

既然知道了问题发生的原因,记下来我们就要想办法解决问题啊,解决的思路就是保证一个线程在调用out方法的时候如果没执行完那么另一个不能执行此方法,换句话说就是只能等待别的线程执行完毕才能执行。

针对线程同步问题java早就有解决方法了,最简单的就是给方法加上synchronized关键字,如下:

 public synchronized void out(String str) {
for (int i = 0; i < str.length(); i++) {
System.out.print(str.charAt(i));
}
System.out.println();
}

这是什么意思呢?加上synchronized关键字后,比如A线程执行out方法就相当于拿到了一把锁,只有获取这个锁才能执行此方法,如果在A线程执行out方法过程中B线程也想插一脚进来执行out方法,对不起此时这是不能够的,因为此时锁在A线程手里,B线程无权拿到这把锁,只有等到A线程执行完后放弃锁,B线程才能拿到锁执行out方法。

为out方法加上synchronized后其就变成了同步方法,普通同步方法的锁是this,也就是当前对象,比如demo中,外部要想调用out方法就必须创建OutPutStr类实例对象o,此时out同步方法的锁就是这个o。

四、同步代码块解决上述问题

我们也可以利用同步代码块解决上述问题,修改out方法如下:

 public void out(String str) {
synchronized (this) {
for (int i = 0; i < str.length(); i++) {
System.out.print(str.charAt(i));
}
System.out.println();
}
}

同步代码块写法:synchronized(obj){},其中obj为锁对象,此处我们传入this,同样方法的锁也为当前对象,如果此处我们传入str,那么这里的锁就是str对象了。

为了说明不同锁带来的影响我们修改OutPutStr代码如下:

 public class OutPutStr {

     public synchronized void out(String str) {
for (int i = 0; i < str.length(); i++) {
System.out.print(str.charAt(i));
}
System.out.println();
} public void out1(String str) { synchronized (str) {
for (int i = 0; i < str.length(); i++) {
System.out.print(str.charAt(i));
}
System.out.println();
}
}
}

很简单我们就是加入了一个out1方法,out方法用同步函数保证同步,out1用同步代码块保证代码块,但是锁我们用的是str。

main代码:

 public static void main(String[] args) {
//
final OutPutStr o = new OutPutStr();
new Thread(new Runnable() { @Override
public void run() {
//
while(true){
o.out("111111111111");
}
}
}).start();
new Thread(new Runnable() { @Override
public void run() {
//
while(true){
o.out1("222222222222");
}
}
}).start();
}

也没什么,就是其中一个线程调用out方法,另一个调用out1方法,运行程序:

111111111111222
222222222222 111111111111222222222222
222222222222

看到了吧,打印信息又出问题了,就是因为out与out1方法的锁不一样导致的,线程A调用out方法拿到this这把锁,线程B调用out1拿到str这把锁,二者互不影响,解决办法也很简单,修改out1方法如下即可:

 public void out1(String str) {

         synchronized (this) {
for (int i = 0; i < str.length(); i++) {
System.out.print(str.charAt(i));
}
System.out.println();
}
}

五、静态函数的同步问题

我们继续修改OutPutStr类,加入out2方法:

 public class OutPutStr {

     public synchronized void out(String str) {
for (int i = 0; i < str.length(); i++) {
System.out.print(str.charAt(i));
}
System.out.println();
} public void out1(String str) { synchronized (this) {
for (int i = 0; i < str.length(); i++) {
System.out.print(str.charAt(i));
}
System.out.println();
}
} public synchronized static void out2(String str) { for (int i = 0; i < str.length(); i++) {
System.out.print(str.charAt(i));
}
System.out.println();
}
}

main中两个子线程分别调用out1,ou2打印信息,运行程序打印信息如下;

 222222222222
222222222222
222222222111111111111
111111111111

咦?又出错了,out2与out方法唯一不同就是out2就是静态方法啊,不是说同步方法锁是this吗,是啊,没错,但是静态方法没有对应类的实例对象依然可以调用,那其锁是谁呢?显然静态方法锁不是this,这里就直说了,是类的字节码对象,类的字节码对象是优先于类实例对象存在的。

将ou1方法改为如下:

 public void out1(String str) {

         synchronized (OutPutStr.class) {
for (int i = 0; i < str.length(); i++) {
System.out.print(str.charAt(i));
}
System.out.println();
}
}

再次运行程序,就会发现信息能正常打印了。

六、synchronized同步方式总结

到此我们就该小小的总结一下了,普通同步函数的锁是this,当前类实例对象,同步代码块锁可以自己定义,静态同步函数的锁是类的字节码文件。总结完毕,就是这么简单。说了一大堆理解这一句就够了。

七、JDK1.5中Lock锁机制解决线程同步

大家是不是觉得上面说的锁这个玩意咋这么抽象,看不见,摸不着的。从JDK1.5起我们就可以根据需要显性的获取锁以及释放锁了,这样也更加符合面向对象原则。

Lock接口的实现子类之一ReentrantLock,翻译过来就是重入锁,就是支持重新进入的锁,该锁能够支持一个线程对资源的重复加锁,也就是说在调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞,同时还支持获取锁的公平性和非公平性,所谓公平性就是多个线程发起lock()请求,先发起的线程优先获取执行权,非公平性就是获取锁与是否优先发起lock()操作无关。默认情况下是不公平的锁,为什么要这样设计呢?现实生活中我们都希望公平的啊?我们想一下,现实生活中要保证公平就必须额外开销,比如地铁站保证有序公平进站就必须配备额外人员维持秩序,程序中也是一样保证公平就必须需要额外开销,这样性能就下降了,所以公平与性能是有一定矛盾的,除非公平策略对你的程序很重要,比如必须按照顺序执行线程,否则还是使用不公平锁为好。

接下来我们修改OutPutStr类,添加out3方法:

 //true表示公平锁,false非公平锁
private Lock lock = new ReentrantLock(); public void out3(String str) { lock.lock();//如果有其它线程已经获取锁,那么当前线程在此等待直到其它线程释放锁。
try {
for (int i = 0; i < str.length(); i++) {
System.out.print(str.charAt(i));
}
System.out.println();
} finally {
lock.unlock();//释放锁资源,之所以加入try{}finally{}代码块,
//是为了保证锁资源的释放,如果代码发生异常也可以保证锁资源的释放,
//否则其它线程无法拿到锁资源执行业务逻辑,永远处于等待状态。
}
}

关键注释都在代码中有所体现了,使用起来也很简单。

八、Lock与synchronized同步方式优缺点

Lock 的锁定是通过代码实现的,而 synchronized 是在 JVM 层面上实现的(所有对象都自动含有单一的锁。JVM负责跟踪对象被加锁的次数。如果一个对象被解锁,其计数变为0。在线程第一次给对象加锁的时候,计数变为1。每当这个相同的线程在此对象上获得锁时,计数会递增。只有首先获得锁的线程才能继续获取该对象上的多个锁。每当线程离开一个synchronized方法,计数递减,当计数为0的时候,锁被完全释放,此时别的线程就可以使用此资源)。

synchronized 在锁定时如果方法块抛出异常,JVM 会自动将锁释放掉,不会因为出了异常没有释放锁造成线程死锁。但是 Lock 的话就享受不到 JVM 带来自动的功能,出现异常时必须在 finally 将锁释放掉,否则将会引起死锁。

在资源竞争不是很激烈的情况下,偶尔会有同步的情形下,synchronized是很合适的。原因在于,编译程序通常会尽可能的进行优化synchronize,另外可读性非常好。在资源竞争激烈情况下,Lock同步机制性能会更好一些。

关于线程同步问题到这里就结束了,java多线程文章只是本人工作以来的一次梳理,都比较基础,但是却很重要的,最近招人面试的最大体会就是都喜欢那些所谓时髦的技术一问基础说的乱七八糟,浪费彼此的时间。好啦,吐槽了几句,本文到此为止,很基础的玩意,希望对你有用。

声明:文章将会陆续搬迁到个人公众号,以后文章也会第一时间发布到个人公众号,及时获取文章内容请关注公众号

Java多线程同步问题:一个小Demo完全搞懂的更多相关文章

  1. java线程间通信:一个小Demo完全搞懂

    版权声明:本文出自汪磊的博客,转载请务必注明出处. Java线程系列文章只是自己知识的总结梳理,都是最基础的玩意,已经掌握熟练的可以绕过. 一.从一个小Demo说起 上篇我们聊到了Java多线程的同步 ...

  2. 泥鳅般的const(一个小Demo彻底搞清楚)

    #include<stdio.h> int main(){     int a = 3;     int b = 5;          /* C标准库函数中最常见格式, 目的是保护參数, ...

  3. Java多线程-同步:synchronized 和线程通信:生产者消费者模式

    大家伙周末愉快,小乐又来给大家献上技术大餐.上次是说到了Java多线程的创建和状态|乐字节,接下来,我们再来接着说Java多线程-同步:synchronized 和线程通信:生产者消费者模式. 一.同 ...

  4. Java多线程同步问题的探究

    一.线程的先来后到——问题的提出:为什么要有多线程同步?Java多线程同步的机制是什么? http://www.blogjava.net/zhangwei217245/archive/2010/03/ ...

  5. 转:关于JAVA多线程同步

    转:http://lanvis.blog.163.com/blog/static/26982162009798422547/ 因为需要,最近关注了一下JAVA多线程同步问题.JAVA多线程同步主要依赖 ...

  6. java多线程同步

    一篇好文:java多线程机制同步原则 概括起来说,Java 多线程同步机制主要包含如下几点:1:如果一个类包含一个或几个同步方法,那么由此类生成的每一个对象都配备一个队列用来容纳那些等待执行同步的线程 ...

  7. java操作xml的一个小例子

    最近两天公司事比较多,这两天自己主要跟xml打交道,今天更一下用java操作xml的一个小例子. 原来自己操作xml一直用这个包:xstream-1.4.2.jar.然后用注解的方式,很方便,自己只要 ...

  8. 模仿京东顶部搜索条效果制作的一个小demo

    最近模仿京东顶部搜索条效果制作的一个小demo,特贴到这里,今后如果有用到可以参考一下,代码如下 #define kScreenWidth [UIScreen mainScreen].bounds.s ...

  9. Visual Studio 2017 - Windows应用程序打包成exe文件(2)- Advanced Installer 关于Newtonsoft.Json,LINQ to JSON的一个小demo mysql循环插入数据、生成随机数及CONCAT函数 .NET记录-获取外网IP以及判断该IP是属于网通还是电信 Guid的生成和数据修整(去除空格和小写字符)

    Visual Studio 2017 - Windows应用程序打包成exe文件(2)- Advanced Installer   Advanced Installer :Free for 30 da ...

随机推荐

  1. exports 和 module.exports

    首先参考一个js的示例 app.js var a = {name: 'nswbmw 1'}; var b = a; console.log(a); console.log(b); b.name = ' ...

  2. HBase shell 命令介绍

    HBase shell是HBase的一套命令行工具,类似传统数据中的sql概念,可以使用shell命令来查询HBase中数据的详细情况.安装完HBase之后,如果配置了HBase的环境变量,只要在sh ...

  3. ES6初体验

    开始学习ES6,打算走全栈这条路了,废话不多说,开始吧. 首先安装node环境,去node官网上面下载node最新版本的,我用的系统是window10,所以我只需要下一步下一步就行了,安装完成后打开c ...

  4. IOS学习4——block代码块

    本文转载自:iOS开发-由浅至深学习block 一.关于block 在iOS 4.0之后,block横空出世,它本身封装了一段代码并将这段代码当做变量,通过block()的方式进行回调.这不免让我们想 ...

  5. iOS Xcode及模拟器SDK下载

    原文: Xcode及模拟器SDK下载 如果你嫌在 App Store 下载 Xcode 太慢,你也可以选择从网络上下载: Xcode下载(Beta版打的包是不能提交到App Store上的) 绝对官方 ...

  6. php面向对象4

    成员属性 基本介绍 成员属性是类的一个组成部分,一般是基本数据类型,也可以是复合类型,资源类型.比如 public $age;就是成员属性 说明 ①成员属性可以是php的任何数据类型(int, flo ...

  7. BZOJ 4818 SDOI2017 序列计数

    刚出炉的省选题,还是山东的. 自古山东出数学和网络流,堪称思维的殿堂,比某地数据结构成风好多了. 废话不说上题解. 1.题面 求:n个数(顺序可更改),值域为[1,m],和为p的倍数,且这些数里面有质 ...

  8. ffmpeg常用命令---转

    1.分离视频音频流 ffmpeg -i input_file -vcodec copy -an output_file_video //分离视频流 ffmpeg -i input_file -acod ...

  9. php-自动过滤、自动填充、自动验证

    最近又学到了一些新技巧,和大家分享下. 第一.当一个表单有很大内容时,我们在表单处理页面接收这些表单的值的时候就会重复 接收,于是就有了自动过滤的解决之法(核心就是把数据表里需要的字段接收) 首先:我 ...

  10. js变量提升与函数提升

    在es6之前,js语言并没有块级作用域,即{}形成的作用域,只有全局作用域和函数作用域,所谓的提升,即是将该变量的声明或者函数的声明提升,举个例子 console.log(global); //und ...