文章目录
应该关注的问题
- 锁是怎么产生的
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();
}
}
- 根据输出的日志记录,很容易发现,厕所同时进了很多人,一个还没有完成,另外一个人就进来了;例如
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
变量的代码上锁,结果如何呢?
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
加在代码块上,锁的还是实例对象,但是这样做的效率比加载实例方法上效率高