Java线程知识:二、锁的简单使用
锁的初步认识
说到锁,相信大家都不陌生,这是我们生活中非常常见的一种东西,它的形状也各式各样。在生活中,我们通常用锁来锁住房子的大门、装宠物的笼子、装衣服的衣柜、以及装着我们一些小秘密的小抽屉......
那么相同的,Java中的锁也各式各样,我们往往按照是否含有某一特性来定义锁,并将锁进行归、分组,具体可分为以下几种:
而这些锁在Java中的具体实现都离不开synchronized
关键字和java.util.concurrent.locks.Lock
接口类,本篇随笔就以synchronized
关键字和Lock
接口的实现类ReentrantLock
来展示对锁的简单使用。
synchronized 内置锁(隐式锁)
作为Java中53个关键字的其中之一,synchronized
占有举重若轻的地位,它是Java语言本身为我们提供的一种同步锁,所以又被称为内置锁或 隐式锁。
1、从语法维度上来讲,synchronized
一共有三种用法:
- 静态方法上加关键字
public static synchronized void add(){}
- 实例方法上加关键字
public synchronized void add(){}
- 方法中使用同步代码块
public void add(){
synchronized(this){}
}
在讲解这些用法之前,我们先来看一段代码:
/**
* @author cai
*/
public class SynDemo {
private static int num = 0;
private static final int ADD_NUM = 2000;
private static final int THREAD_NUM = 5;
private static class UserThread extends Thread {
private SynDemo synDemo;
public UserThread(String threadName, SynDemo synDemo) {
super(threadName);
this.synDemo = synDemo;
}
@Override
public void run() {
synDemo.add();
}
}
public void add() {
for (int i = 0; i < ADD_NUM; i++) {
num++;
}
System.out.println(Thread.currentThread().getName()
+ " 运行完之后的结果为:" + num);
}
public static void main(String[] args) {
// 开启5个线程,使num累计计数到10000
SynDemo synDemo = new SynDemo();
for (int i = 0; i < THREAD_NUM; i++) {
UserThread userThread = new UserThread("thread_" + i, synDemo);
userThread.start();
}
}
}
如代码中一般,我们开启5个线程,并循环使num
变量累计计数,同时打印每个线程运行完之后,num
变量的数值,那么我们所期待的结果一定是这样的:
然而这是在没有考虑并发的情况下的理想结果,但现实却是:在线程thread_0
还没从循环中脱离时,线程thread_1
已经进入了循环,从而导致了num
变量的多次计数,所以就变成了以下结果(运行结果不止这一种,我只是选取了随机的一种,以下代码的运行结果都是这样。):
那么我们用上synchronized
关键字,再来看看运行结果:
1.1 实例方法上加关键字
/**
* 在实例方法 (普通方法) 上加关键字
*/
public synchronized void add() {
for (int i = 0; i < ADD_NUM; i++) {
num++;
}
System.out.println(Thread.currentThread().getName() + " 运行完之后的结果为:" + num);
}
这时的运行结果就变成了这样:
这里的线程顺序问题不用纠结,因为synchronized
是一种非公平锁,线程不会按顺序去排队,而是争先恐后的去抢这唯一的一把锁,所以每次的运行结果中的线程顺序大多不相同,但num
变量的计数结果确实与我们所期望的结果相符合的。
1.2 静态方法上加关键字
/**
* 在静态方法上加关键字
*/
public static synchronized void add() {
for (int i = 0; i < ADD_NUM; i++) {
num++;
}
System.out.println(Thread.currentThread().getName() + " 运行完之后的结果为:" + num);
}
结果:
1.3 方法中使用同步代码块
public void add() {
synchronized (this){
for (int i = 0; i < ADD_NUM; i++) {
num++;
}
System.out.println(Thread.currentThread().getName() + " 运行完之后的结果为:" + num);
}
}
结果:
从结果上来看,以上三种的加锁方式都能满足我们的需求,使num
变量计数到10000
,但不论在我们日常使用上,还是从Java语言本身的建议上讲,更推荐使用第三种用法,即在方法中使用同步代码块的用法,这种方法的性能要比前面两种更好一些,至于为什么,就是属于JVM
层次的研究了,这里不多赘述。
再回到我们的第三种用法,其实它不止这一种写法,我们可以按上述的代码样式书写:synchronized(this){}
,也可以这样写:synchronized(SynDemo.class){}
,还有private SynDemo synDemo = new SynDemo(); synchronized(synDemo){}
这样的写法。看到这里,肯定有很多人的心里不禁的浮现出三个大字:WTF
? ? ?,这都是些什么玩意!!!!synchronized
到底锁住的是谁!???
那么我们就来从另一个维度来揭露一下。
2、从synchronized锁的是谁的维度来讲,一共有两种情况:
2.1 对象锁
我们这里先保留上面 1.3 中的代码不变,稍稍变动一下main
方法中的代码:
如上图所示,将创建synDemo
对象的代码从for
循环外移入for
循环内,这样的话,我们每次新建线程时所传入的synDemo
对象是不同的,这时候再来看看运行的结果:
这样的结果又和我们的期望大相径庭,那么我们是不是可以认定synchronized(this){}
锁住的就是对象呢?让我们再来看一个实例:
/**
* @author cai
*/
public class SynDemo {
private static int num = 0;
private static final int ADD_NUM = 2000;
private static final int THREAD_NUM = 5;
// 共享的对象
private static SynDemo synDemo = new SynDemo();
private static class UserThread extends Thread {
/*public UserThread(String threadName, SynDemo synDemo) {
super(threadName);
this.synDemo = synDemo;
}*/
public UserThread(String threadName){
super(threadName);
}
@Override
public void run() {
synDemo.add();
}
}
public void add() {
synchronized (synDemo){
for (int i = 0; i < ADD_NUM; i++) {
num++;
}
System.out.println(Thread.currentThread().getName()
+ " 运行完之后的结果为:" + num);
}
}
public static void main(String[] args) {
// 开启5个线程,使num累计计数到10000
// SynDemo synDemo = new SynDemo();
for (int i = 0; i < THREAD_NUM; i++) {
// SynDemo synDemo = new SynDemo();
UserThread userThread = new UserThread("thread_" + i);
userThread.start();
}
}
}
如图,我们将UserThread
类的构造器做一下改变,并将SynDemo
对象共享出来,同时换上第三种写法:private SynDemo synDemo = new SynDemo(); synchronized(synDemo){}
,这时的结果为:
从结果我们可以推断出synchronized(this){}
、private SynDemo synDemo = new SynDemo(); synchronized(synDemo){}
这两种写法中synchronized
锁住的是类的对象:在类的对象相同的情况下,多个线程访问一段加锁( 对象锁 )的代码时,只有一个线程能拿到锁。
2.2 类锁
我们来回到2.1中的最初代码,将synchronized (this) {}
改为synchronized (SynDemo.class){}
:
public void add() {
// synchronized (this) {
synchronized (SynDemo.class){
for (int i = 0; i < ADD_NUM; i++) {
num++;
}
System.out.println(Thread.currentThread().getName()
+ " 运行完之后的结果为:" + num);
}
}
public static void main(String[] args) {
// 开启5个线程,使num累计计数到10000
// SynDemo synDemo = new SynDemo();
for (int i = 0; i < THREAD_NUM; i++) {
SynDemo synDemo = new SynDemo();
UserThread userThread = new UserThread("thread_" + i, synDemo);
userThread.start();
}
}
结果:
由此可见,synchronized (SynDemo.class){}
是对SynDemo
整个类进行加锁,所以即便每个线程传入的synDemo
对象不同,但在运行加锁代码块时,都要去抢夺锁,所以num
变量每次打印的计数值都是符合我们心里的预期的。
讲到这里,肯定会有人好奇:synchronized
另外两种用法锁住的是对象还是类呢?让我们修改一下代码看看:
public synchronized void add() {
// synchronized (this) {
// synchronized (SynDemo.class){
for (int i = 0; i < ADD_NUM; i++) {
num++;
}
System.out.println(Thread.currentThread().getName()
+ " 运行完之后的结果为:" + num);
// }
}
public static void main(String[] args) {
// 开启5个线程,使num累计计数到10000
// SynDemo synDemo = new SynDemo();
for (int i = 0; i < THREAD_NUM; i++) {
SynDemo synDemo = new SynDemo();
UserThread userThread = new UserThread("thread_" + i, synDemo);
userThread.start();
}
}
结果:
public static synchronized void add() {
// synchronized (this) {
// synchronized (SynDemo.class){
for (int i = 0; i < ADD_NUM; i++) {
num++;
}
System.out.println(Thread.currentThread().getName()
+ " 运行完之后的结果为:" + num);
// }
}
结果:
结论
由上面的各种代码的运行结果,我们可以得出以下结论
public synchronized void add(){}
、synchronized(this){}
、private SynDemo synDemo = new SynDemo(); synchronized(synDemo){}
这三种写法中的synchronized
锁住的都是对象,即对象锁public static synchronized void add(){}
、synchronized(SynDemo.class){}
这两种写法中的synchonized
锁住的都是类,即类锁- 建议: 在日常工作或学习中,使用代码块加锁的方式。
Lock 显示锁
与synchronized
不同,Lock
是JDK1.5
为我们提供的一个api
,所以它与synchronized
一明一暗,被称为显示锁。
Lock
作为一个接口,有着多个实现类:ReadLock
、ReentrantLock
、WriteLock
......
而我们今天的主角便是:ReentrantLock
,先来看看如何使用:
private static Lock lock = new ReentrantLock();
public void add() {
// 拿到锁
lock.lock();
try {
for (int i = 0; i < ADD_NUM; i++) {
num++;
}
System.out.println(Thread.currentThread().getName()
+ " 运行完之后的结果为:" + num);
} finally {
// 释放锁
lock.unlock();
}
}
与synchronized
不同,lock
并没有类锁和对象锁的分类,它的用法也是非常的简单,lock()
方法是当前线程拿到锁,unlock()
方法是当前线程释放锁。是的,lock
与synchronized
最大的不同就是lock
需要线程自己去释放锁,而synchronized
是JVM
帮我们释放锁。如果当前拿到锁的线程不及时的调用unlock()
方法时,程序将不会终止,所有的线程都会卡在方法外。
我们先来看看上面代码的运行结果:
我们再将unlock()
方法注释掉看看:
private static Lock lock = new ReentrantLock();
public void add() {
// 拿到锁
lock.lock();
try {
for (int i = 0; i < ADD_NUM; i++) {
num++;
}
System.out.println(Thread.currentThread().getName()
+ " 运行完之后的结果为:" + num);
} finally {
// 释放锁
// lock.unlock();
}
}
结果:
正如上面所说的那般,程序不会终止,仅有一个线程打印了结果。
结论
- 使用
lock
锁时,必须在try{}
代码块之前调用lock()
方法,并在finally{}
代码块中调用unlock()
方法及时的释放锁。
最后
synchronized
与Lock
不论在本质还是用法上面都有很多的不同,不是一两句就能讲清楚的,在以后的随笔中,我会逐步的去分享Lock
中的各种方法,会将sychronized
和Lock
的不同做个最后的总结,这也是非常重要的一个知识点。
- 若有转载,请标明原处。如若有错,也欢迎大家前来指正。
- 感谢支持:https://www.cnblogs.com/qiu18359243869/p/11021225.html
Java线程知识:二、锁的简单使用的更多相关文章
- java线程之二(synchronize和volatile方法)
要说明线程同步问题首先要说明Java线程的两个特性,可见性和有序性.多个线程之间是不能直接传递数据交互的,它们之间的交互只能通过共享变量来实现.拿上篇博文中的例子来说明,在多个线程之间共享了Count ...
- Java线程知识
概念 线程生命周期 Java线程模型 线程方法 线程优先级 线程同步 线程在多任务处理应用程序中有着至关重要的作用 概念 基本概念 进程:在操作系统中每个独立运行的程序就是一个进程 线程:程序执行的一 ...
- java 基础知识二 基本类型与运算符
java 基础知识二 基本类型与运算符 1.标识符 定义:为类.方法.变量起的名称 由大小写字母.数字.下划线(_)和美元符号($)组成,同时不能以数字开头 2.关键字 java语言保留特殊含义或者 ...
- Java线程知识拾遗
知识回顾 进程与线程是常常被提到的两个概念.进程拥有独立的代码段.数据空间,线程共享代码段和数据空间,但有独立的栈空间.线程是操作系统调度的最小单位,通常一个进程会包含一个或多个线程.多线程和多进程都 ...
- Java 线程安全 与 锁
Java 线程安全 与 锁 多线程内存模型 线程私有栈内存 每个线程 私有的内存区域 进程公有堆内存 同一个进程 共有的内存区域 为什么会有线程安全问题? 多个线程同时具有对同一资源的操作权限,又发生 ...
- Java线程同步与锁
一.synchronized synchronized锁什么?锁对象.可能锁对象包括: this, 临界资源对象,Class类对象. 1,同步方法 synchronized T methodName( ...
- 【Thread】java线程之对象锁、类锁、线程安全
说明: 1.个人技术也不咋滴.也没在项目中写过线程,以下全是根据自己的理解写的.所以,仅供参考及希望指出不同的观点. 2.其实想把代码的github贴出来,但还是推荐在初学的您多亲自写一下,就没贴出来 ...
- 【Java线程安全】锁
Java都有哪些锁? synchronized 和 reentranlock是最常见的,其中前者又JVM提供实现,后者有专门对应的java.util.concurrent包提供:同时后者功能更加丰富. ...
- Java基础知识之锁
Java中实现锁的方式有多种,并且锁的分类也有很多,这篇文章会从锁分类方面简单介绍各分类的锁的特点. 悲观锁和乐观锁 悲观锁:先假设别人也会对数据就行修改,所以先获得锁再进行操作.一个县城在获得锁之后 ...
随机推荐
- centos-pip不存在安装失败的解决办法
1.centos下安装pip失败 2.解决办法 需要先安装扩展源EPEL. EPEL(http://fedoraproject.org/wiki/EPEL) 是由 Fedora 社区打造,为 RHEL ...
- Docker Compose部署 EFK(Elasticsearch + Fluentd + Kibana)收集日志
简述 本文用于记录如何使用Docker Compose部署 EFK(Elasticsearch + Fluentd + Kibana) 收集Docker容器日志,使用EFK,可以无侵入代码,获得灵活, ...
- Java编程技术之浅析Java容器技术
Java容器 集合是一种存储数据的容器,是Java开发中使用最频繁的对象类型之一. 或许提起Collection,都会第一时间意识到List和Set以及Map等相关关键词.因为这几乎是我们日常开发里接 ...
- C/C++代码覆盖率统计工具:gcov&&gcovr安装和简单使用
gcov安装 Linux ver: gcov是gcc的自带功能 属于GNU 不用特别安装 Windows ver: 在windows下安装可以使用gcov的gcc 之前试过mingw和Cygwin64 ...
- day45 数据库基础
目录 一.存储引擎 二.数据类型 1 整形 2 浮点型 3 字符类型 3.1 类型 3.2 举例验证区别 3.3 对比优缺点 4 日期类型 5 枚举和集合类型 一.存储引擎 不同的存储引擎对应着不同的 ...
- mysql修改密码的三种方式
- 我终于弄懂了Python的装饰器(四)
此系列文档: 1. 我终于弄懂了Python的装饰器(一) 2. 我终于弄懂了Python的装饰器(二) 3. 我终于弄懂了Python的装饰器(三) 4. 我终于弄懂了Python的装饰器(四) 四 ...
- java 面向对象(十二):面向对象的特征二:继承性 (一) 前言
1.为什么要有类的继承性?(继承性的好处) * ① 减少了代码的冗余,提高了代码的复用性 * ② 便于功能的扩展 * ③ 为之后多态性的使用,提供了前提图示: 2.继承性的格式:class A ext ...
- jquery文件表单上传
1. 引入jquery文件 <script src="js/jquery-2.1.1.min.js"></script> 2. 创建form表单,如下: ...
- 想用@Autowired注入static静态成员?官方不推荐你却还偏要这么做
生命太短暂,不要去做一些根本没有人想要的东西.本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈.MyBatis.JVM.中间件等小而美的专栏供以免费学习 ...