文章目录
应该关注的问题
- 锁是怎么产生的
synchronized
关键字锁的是谁- 需要等待的是谁
- 怎么上锁才是对的
锁是怎么产生的
- 这里你只需要记住一句话:
- 每个对象产生的时候自己就带了一把锁
- 我知道你以前可能想的是:我们需要创建一把锁,然后把。。。锁住,其实不是的,每个对象在产生的时候,自己就有一把锁,只是你看不见,只有再用
synchronized
的时候才能有用 - 比如现在你有一个
Toilet
类,那么这个Toiltet
创建的实例自身就带锁;你可以这么理解,每个厕所被生产出来的时候,大门上就会带一把锁。
synchronize 关键字锁的是谁
synchronized
可以锁三种东西:synchronized
关键字加载静态方法上锁的是类对象synchronized
关键字加载实例方法上锁的是这个实例(也就是this
)synchronized
关键字加载代码块上锁的是依然是实例对象,只不过相比于第二种方式,这样的效率会更高一些
锁非静态方法(实例方法)
- 对一个实例对象进行限制,我们看下面的代码
- 构造一个
Toilet
类,创建一个Toilet
对象,名为:城南厕所;就像我们上面提到的,它在创建的时候就会有一把锁 - 创建一个
Person
类,产生1000
个排队上厕所的人。 - 这
1000
个排队上厕所的人争夺的是这个 “城南厕所” 的对象。 - 如果我们想让这些人在上厕所的时候不被打扰,应该给哪个代码段加
synchronized
关键字呢?当然是把对toilet
变量 “写” 操作的代码块加上synchronized
;我们来看代码示例,毕竟语言描述太不具体:talk is cheap, show me the code
- 构造一个
Producer 来产生排队上厕所的人
- 每个人都是一个线程
package Test;
import static java.lang.Thread.interrupted;
import static java.lang.Thread.sleep;
public class Producer implements Runnable{
Toilet toilet;
public Producer(Toilet toilet){
this.toilet = toilet;
}
@Override
public void run() {
int counter = 0;
while (!interrupted()){
new Thread(new Person(""+counter, toilet)).start();
counter ++;
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Toilet 公共厕所,构建实例“城南厕所”
package Test;
import static java.lang.Thread.interrupted;
import static java.lang.Thread.sleep;
public class Toilet implements Runnable{
public Person getPerson() {
return person;
}
public void setPerson(Person person) {
this.person = person;
}
private Person person;
String name;
public Toilet(String toiletName){
this.name = toiletName;
}
public static void main(String[] args) {
Toilet toilet = new Toilet("城南厕所");
new Thread(toilet).start();
Producer producer = new Producer(toilet);
new Thread(producer).start();
}
@Override
public void run() {
while (!interrupted()){
if (person != null){
System.out.println(person.name + "is peeing");
System.out.println("please, wait");
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(person.name + "leaves the toilet");
person = null;
}
}
}
}
Person 上厕所
package Test;
public class Person implements Runnable {
String name;
Toilet toilet;
public Person(String name, Toilet toilet){
this.name = name;
this.toilet = toilet;
System.out.println(name + "is creating");
}
public void pee(){
this.toilet.setPerson(this);
}
@Override
public void run() {
pee();
}
}
0is creating
0is peeing
please, wait
1is creating
2is creating
3is creating
4is creating
5is creating
6is creating
7is creating
8is creating
9is creating
9leaves the toilet
10is creating
10is peeing
please, wait
11is creating
12is creating
13is creating
14is creating
15is creating
16is creating
17is creating
18is creating
19is creating
19leaves the toilet
20is creating
20is peeing
please, wait
21is creating
22is creating
- 根据输出的日志记录,很容易发现,厕所同时进了很多人,一个还没有完成,另外一个人就进来了;例如
0
号并没有走出toilet
,反而是9
号 先离开toilet
。 - 我们在很多例子中都知道这是因为线程安全问题导致的,我们要用
synchronized
关键字来实现同步,但你真的知道加在哪里么?
不易察觉的错误
- 按照上面我们说的, toilet 之所以被很多人同时访问,肯定是因为
toilet
的person
属性同时被多个人修改,那我把那段代码通过synchronized
锁起来肯定就没事了。所以toilet
代码如下修改:
@Override
public void run() {
while (!interrupted()){
// 在这里锁起来,保证这段代码是安全的
synchronized (this){
if (person != null){
System.out.println(person.name + "is peeing");
System.out.println("please, wait");
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(person.name + "leaves the toilet");
person = null;
}
}
- 我们只修改了
run
中的内容,把修改toilet
变量的代码上锁,结果如何呢?
0is creating
0is peeing
please, wait
1is creating
2is creating
3is creating
4is creating
5is creating
6is creating
7is creating
8is creating
9is creating
9leaves the toilet
10is creating
10is peeing
please, wait
11is creating
12is creating
13is creating
14is creating
15is creating
16is creating
17is creating
18is creating
19is creating
19leaves the toilet
20is creating
20is peeing
please, wait
21is creating
22is creating
23is creating
24is creating
25is creating
26is creating
27is creating
28is creating
29is creating
29leaves the toilet
30is creating
30is peeing
please, wait
31is creating
32is creating
33is creating
34is creating
35is creating
Process finished with exit code -1
- 你会发现,其实问题并没有被解决。
- 乍一看好像是
sleep
的问题。是不是因为sleep
的时候,线程把锁释放了,然后让其他的线程访问了呢? - 当然不是,
sleep
的时候,线程依然持有锁,这个部分大家可以去看sleep
和wait
的区别,由于这块不是本文重点,所以不展开讲。 - 那究竟是什么原因导致这块代码没有被锁住呢?
答案揭晓
- 看似在下面的代码中你锁住了
person
不能被多个线程随意修改,但其实根本没有被锁住,因为上面的setPerson
方法是线程不安全的,换句话说,没有用synchronized
关键字限制。这样就导致了在多个Person
的线程中,他们都可以用setPerson
方法同时地修改toilet
的person
属性,导致toilet
的person
还是在不断地被修改。
- 所以我们要保证线程安全就要使用下面的代码,把真正关乎
toilet
中person
属性修改的方法给锁住,就能够保证绝对安全了。
正确操作
package Test;
import static java.lang.Thread.interrupted;
import static java.lang.Thread.sleep;
public class Toilet implements Runnable{
public Person getPerson() {
return person;
}
public synchronized void setPerson(Person person) {
this.person = person;
}
private Person person;
String name;
public Toilet(String toiletName){
this.name = toiletName;
}
public static void main(String[] args) {
Toilet toilet = new Toilet("城南厕所");
new Thread(toilet).start();
Producer producer = new Producer(toilet);
new Thread(producer).start();
}
@Override
public void run() {
while (!interrupted()){
synchronized (this){
if (person != null){
System.out.println(person.name + "is peeing");
System.out.println("please, wait");
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(person.name + "leaves the toilet");
person = null;
}
}
}
}
}
锁静态方法
- 当
synchronized
关键字放在静态方法上,就是对整个类对象进行限制。 - 通过厕所的方法进行说明的话就是:
- 在上面的例子中,如果很多人排队在 “城南厕所” 门口,当
synchronized
加在非静态的代码上时代表一个人上城南厕所,其他人不能上城南厕所,但是如果这时候有 “城北厕所”,他们可以去 “城北厕所” - 但是如果 synchronized 加在了静态方法上。那么当一个人进了 “城南厕所”,“城北厕所” 也会上锁,从而导致一个厕所锁住,所有厕所都不能上。换句话说,把这个类的所有实例都锁住了。
- 在上面的例子中,如果很多人排队在 “城南厕所” 门口,当
锁代码块
synchronized
加在代码块上,锁的还是实例对象,但是这样做的效率比加载实例方法上效率高