创建Maven项目 test1217
。
打开 pom.xml
文件,添加如下依赖:
<dependency>
<groupId>org.apache.curatorgroupId>
<artifactId>curator-recipesartifactId>
<version>5.4.0version>
dependency>
<dependency>
<groupId>org.apache.curatorgroupId>
<artifactId>curator-frameworkartifactId>
<version>5.4.0version>
dependency>
考虑一个应用场景:N个进程同时运行,并发访问某资源。
为了简单起见,用并发线程来模拟多进程。用整数10代表10个资源项。
如果不考虑并发冲突问题,代码如下:
package pkg4;
public class Test1 {
private static int res = 10;
public static void foo() {
while (res > 0) {
System.out.println(Thread.currentThread().getName() + " : " + res);
res--;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new Thread(() -> {
foo();
}).start();
}
}
}
运行结果如下:
Thread-2 : 10
Thread-0 : 10
Thread-1 : 10
Thread-2 : 7
Thread-1 : 6
Thread-0 : 5
Thread-2 : 4
Thread-1 : 4
Thread-0 : 2
Thread-2 : 1
Thread-0 : 1
Thread-1 : 1
Process finished with exit code 0
该程序模拟了3个客户端应用,并发访问资源。具体逻辑为:打印现有的资源数量,并消耗一个资源。
代码逻辑里没有做并发控制,从打印结果可见,多个客户端可能会获取到同一份资源,造成错误。
具体问题包括:
res > 0
在并发情况下,即使判断还有资源,后面打印资源数量以及消耗资源时,可能已经是0了。res--
不是原子操作,在并发情况下,可能会产生“字撕裂”的问题(如果没记错可以使用 volatile
关键字解决)。要解决并发冲突,就要引入同步机制。最简单的办法是用 synchronized
关键字修饰 foo()
方法,但是这样做太暴力了,因为 while
循环是在 foo()
方法里面的,这种做法会导致只能有一个客户端获取所有资源。
因此,需要把“访问资源”的逻辑提出来,单独创建一个 synchronized
方法,如下:
package pkg4;
public class Test2 {
private static int res = 10;
public synchronized static boolean bar() {
if (res > 0) {
System.out.println(Thread.currentThread().getName() + " : " + res);
res--;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return true;
} else {
return false;
}
}
public static void foo() {
while (true) {
boolean succ = bar();
if (!succ) break;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new Thread(() -> {
foo();
}).start();
}
}
}
运行结果如下:
Thread-0 : 10
Thread-2 : 9
Thread-1 : 8
Thread-2 : 7
Thread-0 : 6
Thread-2 : 5
Thread-1 : 4
Thread-2 : 3
Thread-0 : 2
Thread-1 : 1
这回没有问题,多个客户端无序的交替获取资源。
注意:循环里面在调用 bar()
方法之后,最好sleep一会儿,给别的客户端机会,否则貌似当前客户端会继续获取下一个资源,别的客户端抢占不过它。
使用Curator的锁机制,如下:
package pkg4;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;
import java.util.concurrent.TimeUnit;
public class Test3 {
private static int res = 10;
public static boolean bar() {
if (res > 0) {
System.out.println(Thread.currentThread().getName() + " : " + res);
res--;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return true;
} else {
return false;
}
}
public static void foo() {
RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 10);
CuratorFramework zkClient = CuratorFrameworkFactory.builder()
.connectString("localhost:2181")
.sessionTimeoutMs(60 * 1000)
.connectionTimeoutMs(15 * 1000)
.retryPolicy(retryPolicy)
.namespace("test1217")
.build();
zkClient.start();
try {
InterProcessMutex lock = new InterProcessMutex(zkClient,"/myresource");
while (true) {
boolean gotLock = false;
try {
gotLock = lock.acquire(3, TimeUnit.SECONDS);
} catch (Exception e) {
gotLock = false;
}
if (gotLock) {
try {
boolean succ = bar();
if (!succ) break;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} finally {
try {
lock.release();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
} finally {
zkClient.close();
}
}
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new Thread(() -> {
foo();
}).start();
}
}
}
运行结果如下:
Thread-1 : 10
Thread-0 : 9
Thread-2 : 8
Thread-1 : 7
Thread-0 : 6
Thread-2 : 5
Thread-1 : 4
Thread-0 : 3
Thread-2 : 2
Thread-1 : 1
可以看到3个客户端是严格交替获取锁,这是因为客户端一旦释放锁,其它正在等待的客户端就会立即获取锁,而本客户端再等待锁时,其顺序号是最大的,所以最晚获取锁。
前面用 synchronized
方法时,客户端交替是无序的,因为线程调度是无序的。
特别需要注意的是,3个客户端各自拥有自己的 zkClient
和 lock
实例,客户端之间没有任何关联,这是为了模拟客户端进程之间没有任何关联。客户端各自独立访问Zookeeper。
main()
方法和 bar()
方法都没有变化( bar()
方法去掉了 synchronized
关键字)。只有 foo()
方法有修改。其实修改也很简单直接,就是在调用 bar()
的前后添加了获取锁和释放锁的操作,只不过加了一些 try...catch...finally
,显得代码多了一些。
此外,在程序运行期间,可以查看 /test1217/myresource
节点,比如:
ls /test1217/myresource
[_c_b6ad09df-597c-418f-bace-619a9a16369d-lock-0000000004, _c_c4a832f5-fb5c-432d-bf8f-bd7010b8571d-lock-0000000003, _c_d0e6779f-9d87-41ad-a66b-4b8eeab18bae-lock-0000000000]
get /test1217/myresource/_c_d0e6779f-9d87-41ad-a66b-4b8eeab18bae-lock-0000000000
127.0.1.1
可见,有3个顺序节点。
程序运行结束后,过一会儿再看,就会报错说 Node does not exist: /test1217/myresource
,可见是临时节点。
注:要想查看节点,最好把sleep时间设置大一些,或者设置断点,否则来不及看。
我在网上看到一些Curator分布式锁的示例代码,比如:
package com.itheima.curator;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;
import java.util.concurrent.TimeUnit;
public class Ticket12306 implements Runnable{
private int tickets = 10;//数据库的票数
private InterProcessMutex lock ;
public Ticket12306(){
//重试策略
RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 10);
//2.第二种方式
//CuratorFrameworkFactory.builder();
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("192.168.149.135:2181")
.sessionTimeoutMs(60 * 1000)
.connectionTimeoutMs(15 * 1000)
.retryPolicy(retryPolicy)
.build();
//开启连接
client.start();
lock = new InterProcessMutex(client,"/lock");
}
@Override
public void run() {
while(true){
//获取锁
try {
lock.acquire(3, TimeUnit.SECONDS);
if(tickets > 0){
System.out.println(Thread.currentThread()+":"+tickets);
Thread.sleep(100);
tickets--;
}
} catch (Exception e) {
e.printStackTrace();
}finally {
//释放锁
try {
lock.release();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
package com.itheima.curator;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.cache.*;
import org.apache.curator.retry.ExponentialBackoffRetry;
public class LockTest {
public static void main(String[] args) {
Ticket12306 ticket12306 = new Ticket12306();
//创建客户端
Thread t1 = new Thread(ticket12306,"携程");
Thread t2 = new Thread(ticket12306,"飞猪");
t1.start();
t2.start();
}
}
想法和疑问: