从Java线程到kotlin协程之线程同步(synchronized关键字)
Posted XeonYu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从Java线程到kotlin协程之线程同步(synchronized关键字)相关的知识,希望对你有一定的参考价值。
上一篇:
线程的同步
简单来讲,当多个线程要对同一个内存地址进行操作时(一般都是写操作),同一时间只能有一个线程对该内存地址进行操作,其他线程不可以对该内存地址进行操作,此时,其他线程处于BLOCKED (阻塞)状态,这个就是线程同步。
线程同步主要是为了解决数据安全的问题。
我们先来看个经典的卖票例子。
举个例子,总共有30张票,分5个窗口同时售卖,不加同步的代码如下
object Ticket {
/*总共票数*/
var totalCount = 30
/*当前售出的票数*/
private var currentNumber = 1
/*卖票*/
fun sale() {
if (totalCount == 0) {
println("${Thread.currentThread().name} 票卖完了")
return
}
totalCount--
println("${Thread.currentThread().name} 卖出第${currentNumber}张票,剩余${totalCount}张票")
currentNumber++
TimeUnit.MILLISECONDS.sleep(100)
}
}
main方法如下,模拟5个窗口同时卖票
fun main() {
for (i in 1..5) {
Thread({
while (Ticket.totalCount > 0) {
Ticket.sale()
}
}, "窗口" + i).start()
}
}
运行结果如下:
可以看到,卖票的数据明显是不对的。
原因是多个线程同时对Ticket中的count做了操作,造成了数据不安全,导致其他线程访问的数据不是正确的数据。
像这种多个线程同时对同一个内存地址做操作,为了保证数据安全,我们就需要做线程同步操作
synchronized 关键字
synchronized是JVM提供的关键字,该关键字相当于给资源上锁,要访问资源就必须先拿到锁,如果锁被其他线程持有,则当前线程处于BLOCKED(阻塞)状态,等待锁释放,然后去争夺锁获得执行机会。
synchronized 可以用在以下代码中
- 实例方法(非静态方法)
- 静态方法
- 代码块
还是上面的代码,用Java版本加上synchronized 关键字来看一下
public class TestSynchronized {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
while (Ticket.totalCount > 0) {
Ticket.sale();
}
}, "窗口" + i).start();
}
}
}
class Ticket {
public static int totalCount = 30;
private static int number = 1;
/*卖票*/
public static synchronized void sale() {
if (totalCount == 0) {
System.out.println(Thread.currentThread().getName() + "票卖完了");
return;
}
totalCount--;
System.out.println(Thread.currentThread().getName() + "卖出了第" + number + "张票,剩余" + totalCount + "张票");
number++;
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
可以看到,我们给sale静态方法加上了synchronized关键字,下面来看看运行结果
可以看到,这下数据就是正常的了。
需要注意的是,在使用synchronized的时候,要注意synchronized锁的资源是否是同一个,只有锁的资源是同一个,同步代码才会生效。简单来说,要想实现同步,多个线程竞争的锁要是独一份。
上面我们那个是锁的 静态方法,下面我们来试试锁代码块
class Ticket {
public static int totalCount = 30;
private static int number = 1;
/*卖票*/
public static void sale() {
synchronized (Ticket.class) {
if (totalCount == 0) {
System.out.println(Thread.currentThread().getName() + "票卖完了");
return;
}
totalCount--;
System.out.println(Thread.currentThread().getName() + "卖出了第" + number + "张票,剩余" + totalCount + "张票");
number++;
}
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上面我们同步代码块锁住的是Ticket.class,我们只需要锁住需要被同步的代码即可。
我们再来看看运行结果:
可以看到,结果是符合预期的。
下面我们来试试锁住不同的资源试一下,代码如下
class Ticket {
public static int totalCount = 30;
private static int number = 1;
/*卖票*/
public static void sale() {
synchronized (new Ticket()) {
if (totalCount == 0) {
System.out.println(Thread.currentThread().getName() + "票卖完了");
return;
}
totalCount--;
System.out.println(Thread.currentThread().getName() + "卖出了第" + number + "张票,剩余" + totalCount + "张票");
number++;
}
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上面代码我们的同步代码块锁是每次都new一个对象,这样,锁肯定不是同一个。再来看下运行结果:
运行结果可以看到,数据又变成不正确的了。
锁实例方法用法也是类似,代码如下:
public class TestSynchronized {
public static void main(String[] args) {
Ticket ticket = new Ticket();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
while (ticket.totalCount > 0) {
ticket.sale();
}
}, "窗口" + i).start();
}
}
}
class Ticket {
public int totalCount = 30;
private int number = 1;
/*卖票*/
public synchronized void sale() {
if (totalCount == 0) {
System.out.println(Thread.currentThread().getName() + "票卖完了");
return;
}
totalCount--;
System.out.println(Thread.currentThread().getName() + "卖出了第" + number + "张票,剩余" + totalCount + "张票");
number++;
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果如下:
可以看到,结果是正常的。
下面我们再来看看不同实例的同步方法运行是什么样的。
main方法代码如下:
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
Ticket ticket = new Ticket();
while (ticket.totalCount > 0) {
ticket.sale();
}
}, "窗口" + i).start();
}
}
运行结果如下
这个就相当于,起了5个线程,每个线程单独执行任务,跟同步没啥关系了。
所以,在使用synchronized关键字的时候,我们要特别注意线程争夺的 锁 是否是同一个。
还是上面的代码,我们只需要保证,不同线程拿到的实例是同一个就行。
这就是日常开发中很常见的单例模式。
下面我们来改造下代码,如下:
class Ticket {
private static Ticket ticket = null;
public int totalCount = 30;
private int number = 1;
private Ticket() {
}
/*线程安全的单例模式*/
public static Ticket newInstance() {
if (ticket == null) {
synchronized (Ticket.class) {
if (ticket == null) {
synchronized (Ticket.class) {
ticket = new Ticket();
}
}
}
}
return ticket;
}
/*卖票*/
public void sale() {
synchronized (Ticket.class) {
if (totalCount == 0) {
System.out.println(Thread.currentThread().getName() + "票卖完了");
return;
}
totalCount--;
System.out.println(Thread.currentThread().getName() + "卖出了第" + number + "张票,剩余" + totalCount + "张票");
number++;
}
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
main方法
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
Ticket ticket = Ticket.newInstance();
System.out.println("ticket对象地址" + ticket);
while (ticket.totalCount > 0) {
ticket.sale();
}
}, "窗口" + i).start();
}
}
我们再来看下运行结果:
可以看到,由于我们将获取Ticket对象写成了线程安全的单例模式,所以,就不会出现5个对象各自执行的情况了。
下面简单用kotlin实现一下同样的效果,代码如下:
fun main() {
for (i in 1..5) {
val ticket = Ticket.newInstance
println("ticket = ${ticket}")
Thread({
while (ticket.totalCount > 0) {
ticket.sale()
}
}, "窗口" + i).start()
}
}
class Ticket private constructor() {
/*总共票数*/
var totalCount = 30
/*当前售出的票数*/
private var currentNumber = 1
companion object {
/*线程安全的懒汉式单例*/
val newInstance by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { Ticket() }
}
/*卖票*/
fun sale() {
synchronized(Ticket::class.java) {
if (totalCount == 0) {
println("${Thread.currentThread().name} 票卖完了")
return
}
totalCount--
println("${Thread.currentThread().name} 卖出第${currentNumber}张票,剩余${totalCount}张票")
currentNumber++
}
TimeUnit.MILLISECONDS.sleep(100)
}
}
运行结果如下:
也是ok的,没有什么问题
好了,synchronized关键字大概就是这些。
如果你觉得本文对你有帮助,麻烦动动手指顶一下,可以帮助到更多的开发者,如果文中有什么错误的地方,还望指正,转载请注明转自喻志强的博客 ,谢谢!
以上是关于从Java线程到kotlin协程之线程同步(synchronized关键字)的主要内容,如果未能解决你的问题,请参考以下文章