文章目录
一、总述
刚刚我们已经学习完了同步代码块,就是将一段代码锁起来,这样就可以解决多线程操作共享数据时带来的数据安全问题。
但是如果我们想要将一个方法里面所有的代码全部锁起来,此时就没有必要去用同步代码块了,我们可以直接将 synchronized
加在方法上,此时这个方法就叫做同步方法。
修饰符 synchronized 返回值类型 方法名(方法参数) {
方法体;
}
特点1:同步方法是锁住方法里面所有的代码
特点2:同步方法的锁对象,我们是不能自己指定的,是Java已经规定好的。
- 如果你当前的方法是非静态的,它的锁对象就是
this
,即当前方法的调用者。 - 如果你当前的方法是静态的,它的锁对象就是
当前类的字节码文件对象
。
二、练习
题目还是刚刚的题目,只不过要求用同步方法完成。
很多同学在写同步方法的时候都会有一个小疑问:不知道把哪些方法写在同步方法中。
技巧:你首先不要写同步方法,而是先写同步代码块,然后再把同步代码块里面的代码去抽取成方法就行了。
需求:
某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
利用同步方法完成
技巧:同步代码块
我们先用同步代码块写,再去改成同步方法。
由于在之前我们用的都是多线程的第一种实现方式,这里就使用第二种实现方式。
由于现在使用的是第二种实现方式,此时 ticket
的前面就没必要去加 static
了。
我们之前一直使用第一种实现方式(继承Thread类),此时它是有可能创建多个对象的,所以如果我想让所有的对象都共享一个 ticket成员变量的值,在前面就需要加static。
但是我们现在使用的是第二种实现方式(实现Runnable接口),在这种方式下,MyRunnable(我们自己写的类)只有可能会创建一次,它是作为一个参数让线程去执行的,所以只会创建一次它的对象。
那么既然只会创建一次,ticket就没有必要再去加static了。
public class MyRunnable implements Runnable {
int ticket = 0;
@Override
public void run() {
}
}
接下来完善 run()
,写 run()
的时候也是有套路的
public class MyRunnable implements Runnable {
int ticket = 0;
@Override
public void run() {
//1.循环
while (true) {
//2.同步代码块(同步方法)
//3.判断共享数据是否到了末尾,如果到了末尾
if (ticket == 100) {
break;
} else {
//4.判断共享数据是否到了末尾,如果没有到末尾
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票!!!");
}
}
}
}
测试类
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
Thread t3 = new Thread(mr);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
接下来改成同步方法。
改写很简单:将synchronized里面所有的代码全都放到方法中即可。
选中synchronized里面所有的代码,ctrl + alt + M ,此时就可以抽取成一个方法了。
public class MyRunnable implements Runnable {
int ticket = 0;
@Override
public void run() {
//1.循环
while (true) {
//2.同步代码块(同步方法)
if (method()) break;
}
}
//this
private synchronized boolean method() {
//3.判断共享数据是否到了末尾,如果到了末尾
if (ticket == 100) {
return true;
} else {
//4.判断共享数据是否到了末尾,如果没有到末尾
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票!!!");
}
return false;
}
}
三、StringBuffer
学习完同步方法后,我们之前有很多知识点其实也就可以解释了。
很多同学在之前肯定听过:字符串在进行拼接的时候,除了 StringBuilder
外,还有一个类:StringBuffer
。
那这个 StringBuffer
跟我们之前学习的 StringBuilder
有什么区别呢?
打开 API帮助文档
来看一下。
这里打开了两个 API帮助文档
,左边是 StringBuilder
,右边是 StringBuffer
往下滑,可以惊讶的发现,方法都是一模一样的。
那为什么Java要定义两个完全一模一样的类呢?
看下面,StringBuilder下面有这样一句话:将 StringBuilder
的实例(即对象的意思)用于多个线程是不安全的。如果需要这样的同步,则建议使用 StringBuffer
。
接下来看下源码,同样左边是 StringBuilder
,右边是 StringBuffer
。
往下拉,找到成员方法。可以发现,StringBuffer
里面每一个成员方法都有 synchronized
,但左边的 StringBuilder
所有的成员方法都没有 synchronized
,这就表示 StringBuffer
是线程安全的,因为它里面所有的方法前面都有 synchronized
,即它里面所有的方法都是同步的。
那我们以后如何选择呢?
如果你的代码是单线程的,不需要考虑多线程中数据安全的情况,就用 StringBuilder
就行了。
但如果是多线程环境下,需要考虑到 数据安全
,这时候就可以选择右边的 StringBuffer
。