一、什么是无锁操作?
- 举个例子
模拟我们计一个数,所有的线程都要共同访问这个数count值,大家知道如果所有线程都要访问这个数的时候,如果每个线程给它往上加了10000,你这个时候是需要加锁的,不加锁会出问题。但是,你把它改成AtomicInteger之后就不用在做加锁的操作了,因为incrementAndGet内部用了cas操作,直接无锁的操作往上递增,无锁的操作效率会更高。
/**
* 解决同样的问题的高效方法,使用AtomXXX类
* AtomXXX类的本身方法都是原子性的,但不能保证多个方法连续调用都是原子性的
*/
public class Test {
/*volatile*/ //int count1 = 0;
AtomicInteger count = new AtomicInteger(0);
/*synchronized*/void m() {
for (int i = 0; i < 10000; i++)
//if count1.get() < 1000
count.incrementAndGet(); //count1++
}
public static void main(String[] args) {
Test t = new Test();
List<Thread> threads = new ArrayList<Thread>();
for (int i = 0; i < 10; i++) {
threads.add(new Thread(t::m, "thread-" + i));
}
threads.forEach((o) -> o.start());
threads.forEach((o) -> {
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(t.count);
}
}
输出:100000
二、效率的比较?
- 举个例子
模拟了很多个线程对一个数进行递增。
第一种:是一个long类型的数,递增的时候加锁;用synchronized,用一个lock,Object lock = new Object();,然后newRunnable(), 依然是一样的,在递增的时候我写的是synchronized (lock),然后计算时间;
第二种:使用AtomicLong可以让它不断的往上递增,这是第二种;每一个线程都new出来,之后每一个线程都做了十万次递增,第一种方式,打印起始时间->线程开始->所有线程结束->打印结束时间->计算最后花了多少时间;
第三种:使用LongAdder,1000个线程,每个线程十万次递增,第三种呢用的是LongAdder,这个LongAdder里面直接就是count3.increment();
public class Test {
static long count2 = 0L;
static AtomicLong count1 = new AtomicLong(0L);
static LongAdder count3 = new LongAdder();
public static void main(String[] args) throws Exception {
Thread[] threads = new Thread[1000];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int k = 0; k < 100000; k++) count1.incrementAndGet();
});
}
long start = System.currentTimeMillis();
for (Thread t : threads) t.start();
for (Thread t : threads) t.join();
long end = System.currentTimeMillis();
//TimeUnit.SECONDS.sleep(10);
System.out.println("Atomic: " + count1.get() + " time " + (end - start));
//-----------------------------------------------------------
Object lock = new Object();
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for (int k = 0; k < 100000; k++)
synchronized (lock) {
count2++;
}
}
});
}
start = System.currentTimeMillis();
for (Thread t : threads) t.start();
for (Thread t : threads) t.join();
end = System.currentTimeMillis();
System.out.println("Sync: " + count2 + " time " + (end - start));
//---------------------------------------------------------
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int k = 0; k < 100000; k++) count3.increment();
});
}
start = System.currentTimeMillis();
for (Thread t : threads) t.start();
for (Thread t : threads) t.join();
end = System.currentTimeMillis();
//TimeUnit.SECONDS.sleep(10);
System.out.println("LongAdder: " + count1.longValue() + " time " + (end - start));
}
}
输出:
Atomic: 100000000 time 1137
Sync: 100000000 time 6154
LongAdder: 100000000 time 186
结论:跑起来对比LongAdder是效率最高的,但是,把线程数变小了LongAdder未必有优势,循环数量少了LongAdder也未必有优势,所以,实际当中用哪种你要考虑一下你的并发有多高。
问题一:为什么Atomic要比Sync快?
因为不加锁,synchronized是要加锁的,有可能它要去操作系统申请重量级锁,所以synchronized效率偏低,在这种情形下效率偏低。
问题二:LongAdder为什么要比Atomicx效率要高呢?
是因为LongAdder的内部做了一个分段锁,类似于分段锁的概念。在它内部的时候,会把一个值放到一个数组里,比如说数组长度是4,最开始是0,1000个线程,也就是每个数组单元就有250个线程,以此类推,每一个都往上递增算出来结果在加到一起。
二、ReentrantLock
- 什么是可重入?
意思就是我锁了一下还可以对同样这把锁再锁一下,synchronized必须是可重入的,不然的话子类调用父类是没法实现的,synchronized方法是可以调用synchronized方法的。锁是可重入的。子类和父类如果是synchronized(this)就是同一把锁,同一个this当然是同一把锁。
- 举个例子
/**
* new Thread(rl::m2).start();时候
* 本例中由于m1锁定this,只有m1ִ执行完毕的时候,m2才能执行
*/
public class Test {
synchronized void m1() {
for (int i = 0; i < 5; i++) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
if (i == 2) m2();
}
}
synchronized void m2() {
System.out.println("m2 ...");
}
public static void main(String[] args) {
Test rl = new Test();
new Thread(rl::m1).start();
try {
TimeUnit.S