1.什么是juc
JUC是java下面的一个包,全称java.util-concurrent,是java1.5发布的一个并发编程的工具包
2.进程和线程
进程:指在系统中正在运行的一个应用程序,程序一旦运行就行一个进程
线程:是操作系统能够进行运算调度的最小单位,被包含在进程中,是进程的实际运作单位
3. wait和sleep的区别
1.wait属于object类,sleep属于Tread类
2.sleep不会释放锁,它也不会占用锁,wait会释放锁
3.wait只能在同步方法或同步块中使用,而sleep可以在任何地方使用
4.并发和并行的区别
并发:同一时刻多个线程同时访问同一个资源。例如:春运抢票,电商秒杀
并行:多项工作同时执行,然后在汇总
5.用户线程和守护线程
如果jvm中还有用户线程,那么jvm就不会退出,如果jvm中有守护线程没有用户线程,那么jvm就会退出,总之,守护线程依赖于用户线程,用户线程退出了,守护线程也会退出,用户线程是独立的,不会因为其他线程的退出而退出
6.lock和synchronized
lock和synchronized都是可重入锁
synchronized是java的关键字,lock是一个类,通过这个类可以实现同步访问
lock和synchronized最大的区别就是,synchronized不需要用户手动释放锁,而lock则需要用户手动释放锁,否则可能会出现死锁的情况
synchronized如果是普通方法,锁的就是实例对象this
如果是静态方法,锁的就是class对象
如果是同步方法,锁的就是synchronized括号里面的配置对象
synchronized例子
/**
* synchronized卖票例子
*/
class Ticket {
//总共票数
private Integer number = 30;
public synchronized void piao(){
if (number>0){
System.out.println(Thread.currentThread().getName()+":卖出"+number--+"还剩:"+number);
}
}
}
public class Tread {
//创建3个线程(卖票员)
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.piao();
}
},"AA").start();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.piao();
}
},"BB").start();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.piao();
}
},"CC").start();
}
}
lock 例子
/**
* lock卖票例子
*/
class Ticket {
//总共票数
private Integer number = 30;
//创建一个可重入锁
private final ReentrantLock lock = new ReentrantLock();
public void piao(){
//lock需要手动上锁
lock.lock();
//用try()防止出现异常
try {
if (number>0){
System.out.println(Thread.currentThread().getName()+":卖出"+number--+"还剩:"+number);
}
//finally无论有无异常都会执行下面的代码
}finally {
//lock需要手动释放锁
lock.unlock();
}
}
}
public class Tread {
//创建3个线程(卖票员)
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.piao();
}
},"AA").start();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.piao();
}
},"BB").start();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.piao();
}
},"CC").start();
}
}
调用start方法,线程是否会创建?
答:不一定,可能创建了,可能没创建,也可能一会创建,start方法源码里面调用了一个叫start0方法,start0方法上有个native,这就代表是否创建线程取决于操作系统,如果操作系统空闲会马上创建,如果很忙,则会等一会给你创建
线程之间的通信小例子
2个线程,一个线程对number变量进行+1操作,一个线程对number变量进行-1操作
/**
* 实现线程之间的通信小demo
* 2个线程,一个线程对number变量进行+1操作,一个线程对number变量进行-1操作
*/
class Ticket {
//number变量
private Integer number = 0;
//+1
public synchronized void incr() throws InterruptedException {
//如果number不等于0,则让它等待
if (number!=0){
this.wait();
}
//代码如果执行到这里就代表number == 0 ,等于0就+1
number++;
System.out.println(Thread.currentThread().getName()+":"+number);
//+1 之后就要 -1
this.notifyAll();
}
//-1
public synchronized void decr() throws InterruptedException {
//如果number 等于0,则让它等待
if (number!=1){
this.wait();
}
//代码如果执行到这里就代表number != 0 ,不等于0就-1
number--;
System.out.println(Thread.currentThread().getName()+"::"+number);
//-1 之后就要 +1
//wait()会让线程挂起,直到通知到它继续执行!挂起的线程会存放到等待队列中,按照wait的先后顺序存放。
//notify()通知等待队列中的第一个线程,notifyAll()通知的是等待队列中的所有线程
this.notifyAll();
}
}
public class Tread {
//创建2个线程
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
ticket.incr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"AA").start();
new Thread(() -> {
try {
for (int j = 0; j < 10; j++) {
ticket.decr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"BB").start();
}
}
synchronized 实现线程通信
2个线程进行没问题,如果4个线程就会出现虚假唤醒的问题this.wait(),从而导致数据异常,解决方法就是把唤醒操作放到循环里面
lock实现线程通信
/**
* lock接口实现线程之间的通信小demo
* 2个线程,一个线程对number变量进行+1操作,一个线程对number变量进行-1操作
*/
class Ticket {
//number变量
private Integer number = 0;
private final Lock lock = new ReentrantLock();
private Condition condition= lock.newCondition();
//+1
public void incr() throws InterruptedException {
try {
//上锁
lock.lock();
//如果number不等于0,则让它等待
while (number!=0){
condition.await();
}
//代码如果执行到这里就代表number == 0 ,等于0就+1
number++;
System.out.println(Thread.currentThread().getName()+":"+number);
//+1 之后就要 -1
condition.signalAll();
}finally {
//解锁
lock.unlock();
}
}
//-1
public synchronized void decr() throws InterruptedException {
//如果number 等于0,则让它等待
try {
lock.lock();
//防止虚假唤醒,将判断条件写到while循环里面
while (number!=1){
condition.await();
}
//代码如果执行到这里就代表number != 0 ,不等于0就-1
number--;
System.out.println(Thread.currentThread().getName()+"::"+number);
//-1 之后就要 +1
//wait()会让线程挂起,直到通知到它继续执行!挂起的线程会存放到等待队列中,按照wait的先后顺序存放。
//notify()通知等待队列中的第一个线程,notifyAll()通知的是等待队列中的所有线程
//condition.signalAll()和this.notifyAll()实现的效果一样,通知等待队列的所有线程
condition.signalAll();
}finally {
lock.unlock();
}
}
}
public class Tread {
//创建2个线程
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
ticket.incr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"AA").start();
new Thread(() -> {
try {
for (int j = 0; j < 10; j++) {
ticket.decr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"BB").start();
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
ticket.incr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"CC").start();
new Thread(() -> {
try {
for (int j = 0; j < 10; j++) {
ticket.decr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"DD").start();
}
}
lock接口实现线程之间的定制化通信 小demo
AA线程打印5次,BB线程打印10次
/**
* lock接口实现线程之间的定制化通信小demo
* AA线程打印5次,BB线程打印10次
*/
class Ticket {
//flag变量
private Integer flag = 1;
private final Lock lock = new ReentrantLock();
private Condition condition1= lock.newCondition();
private Condition condition2= lock.newCondition();
public void print5(Integer loop) throws InterruptedException {
//上锁
lock.lock();
try {
//如果flag不等于1,则让它等待
while (flag!=1){
condition1.await();
}
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+":"+i+"轮询:"+loop);
}
flag = 2 ;
condition2.signal();
}finally {
//解锁
lock.unlock();
}
}
public void print10(Integer loop) throws InterruptedException {
//上锁
lock.lock();
try {
//如果flag不等于2,则让它等待
while (flag!=2){
condition2.await();
}
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":"+i+"轮询:"+loop);
}
flag = 1 ;
condition1.signal();
}finally {
//解锁
lock.unlock();
}
}
}
public class Tread {
//创建2个线程
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
ticket.print5(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"AA").start();
new Thread(() -> {
try {
for (int j = 0; j < 10; j++) {
ticket.print10(j);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"BB").start();
}
}
JUC包下也有对线程不安全的集合的一些处理,让其变成线程安全
众所周之ArrayList是线程不安全的,如果将其变成安全的呢?
1.直接new Vector();通过看源码可以得知,实现的方法就是在方法上加synchronized,实际开发中不会这样解决,因为加synchronized会影响效率
2.第二种处理ArrayList线程不安全的方法
上述两种情况都可以解决ArrayList线程不安全的问题,但是都比较古老,实际开发用的很少,一般都用下述这种
HashSet也是线程不安全的,如果将其变成安全?
new CopyOnWriteArraySet<>() 即可
HashMap?
公平锁和非公平锁
公平锁:每个线程都会执行,效率就相对较低
非公平锁:可能会出现线程饿死的情况,但是效率很高
如果实现公平锁和非公平锁?
就拿上述lock卖票的例子
如果new ReentrantLock()里面可以传入一个Boolean值,如果为true,就是公平锁,如果是false就是非公平锁,默认是非公平锁
可重入锁
可重入锁指的就是 一个线程可以多次获取同一把锁
比如:一个线程在执行一个带锁的方法,该方法中又调用了另一个带着相同锁的方法,该线程可以直接调用执行该方法,无需重新获得锁
死锁
两个或者两个以上的线程在进行争夺资源的时候,造成一种相互等待的情况,没有外力干涉的情况下,无法执行下去,这种状况叫做死锁
//实现死锁的小例子
public static void main(String[] args) {
Object a = new Object();
Object b = new Object();
new Thread(()->{
synchronized (a){
System.out.println(Thread.currentThread().getName()+":已经持有锁A,试图获取锁B");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b){
System.out.println(Thread.currentThread().getName()+":获取锁B");
}
}
},"A").start();
new Thread(()->{
synchronized (b){
System.out.println(Thread.currentThread().getName()+":已经持有锁B,试图获取锁A");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (a){
System.out.println(Thread.currentThread().getName()+":获取锁A");
}
}
},"B").start();
}
由次可以看见程序并没有结束,A,B两个线程相互等待,形成死锁
Callable接口
众所周知,实现线程的方法,除了继承Thead类,实现Runnable接口,使用线程池,还有一个就是实现Callable接口
Callable接口和Runnable接口区别
1.Runnable接口无返回值, Callable接口有返回值,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
实现Callable接口
//实现Callable接口
class myThead2 implements Callable{
@Override
public Integer call() throws Exception {
return 888;
}
}
public class Thead {
public static void main(String[] args) {
//new一个FutureTask
FutureTask<Integer> futureTask = new FutureTask<>(()-> {
System.out.println(Thread.currentThread().getName()+"callable");
return 111;
});
//将其放到Thead中
new Thread(futureTask,"BB").start();
//futureTask.get()可以获取到Callable接口的返回值
Integer integer = futureTask.get();
}
}
JUC下3个辅助类
1.CountDownLatch计数器
countDownLatch是juc包下的一个辅助类,它是一个计数器,开始可以设置一个值,每次调用countDown()方法都会减1,当调用await()方法并且数值为0的时候,才会执行下面的代码
/**
* 用CountDownLatch实现一个小例子
*/
public class Thead {
public static void main(String[] args) throws InterruptedException {
//创建一个CountDownLatch
CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"号同学离开了教室");
//计数-1
countDownLatch.countDown();
},String.valueOf(i)).start();
}
//等待数值为0的时候,停止等待,执行下面的代码
countDownLatch.await();
System.out.println(Thread.currentThread().getName()+"班长锁门离开");
}
}
如果没有使用countDownLatch,班长锁门离开这句话可能就会在前面出现
2.CyclicBarrier循环栅栏
CyclicBarrier循环栅栏,它的作用就是所有线程都等待完成之后才会继续进一步的行动
例子:去聚餐,必须所有人到了之后才能开始吃饭;
要想许愿,必须集齐7颗龙珠等等;
创建一个CyclicBarrier对象,参数有2个,第一个参数是int值,代表参与线程的个数,第二个就是一个Runnable接口,当参与的线程都到了,才会执行下面的lambda表达式里面的代码
/**
* CyclicBarrier循环栅栏
*/
public class Thead {
private static final Integer NUMBER = 7;
public static void main(String[] args){
//创建一个CyclicBarrier对象,参数有2个,第一个参数是int值,代表参与线程的个数
//第二个就是一个runnable,当参与的线程都到了,才会执行下面的lambda表达式里面的代码
CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER,()->{
System.out.println("---集成了7颗龙珠召唤神龙---");
});
for (int i = 1; i < 8; i++) {
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+"号龙珠");
//等待7次才能召唤神龙
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
3.Semaphore
/**
* Semaphore
* 6辆汽车停到3个停车位上
*/
public class Thead {
public static void main(String[] args){
//3个车位
Semaphore semaphore = new Semaphore(3);
//创建6个线程(6辆车)
for (int i = 0; i < 6; i++) {
new Thread(()->{
try {
//抢占
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+":抢到了车位");
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
System.out.println(Thread.currentThread().getName()+"------离开了车位");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//释放
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}
乐观锁和悲观锁
1.区别
悲观锁:每次获取数据的时候,都担心数据会被修改,所以获取数据的时候都会进行加锁,确保在使用过程中数据不会被修改,完成之后在进行解锁,由于数据会被加锁,所以对该数据在进行读写操作的其他线程会进入等待状态
乐观锁:每次获取数据的时候,都不会担心会被修改,所以获取数据的时候不会进行加锁,但是每次更新数据的时候,都会判断该数据是否被其他线程修改过,如果修改过,则不进行数据更新。如果没有被其他线程进行修改的话,则可以进行数据的更新修改。因为没有加锁的缘故,其他线程可以在该期间对其进行读写操作
2.使用场景
悲观锁:比较适合写入操作比较频繁的场所
乐观锁:比较适合读取操作比较频繁的场所
表锁和行锁
1.区别
表锁:对数据库某张表的第一行数据进行操作,但会把整张表都上锁,别人无法对该表的数据进行操作,表锁不会发生死锁
行锁:对数据库某张表的第一行数据进行操作,但只会对该行数据进行上锁,别人除了无法对该行进行操作,对于其他行的数据可以进行操作,行锁会发生死锁的情况
共享锁和独占锁
1.区别
独占锁:又称写锁,指该锁只能被一个线程持有,如果有其他线程想要持有,必须要等到占用该锁的线程释放掉才行。
共享锁:又称读锁,指对于某个资源,可以同时被多个线程占用
读写锁都可能发生死锁的情况
读锁死锁情况例子:
有A,B两个线程,A线程在读取数据的时候,B线程也在读取数据,那如果A线程在读取的时候想要对数据进行修改,那么A线程就要等到B线程读取完成之后才能进行修改,恰巧的是B线程在读取数据的时候想要对数据进行修改,那么B线程也要等到A线程读取完成之后,才能修改,这种情况下就会发生读锁死锁的情况。
写锁可能发生死锁的情况:
假设有A,B两个线程,
A线程对数据1进行写操作,因为写锁的特性,其他线程是在线程A没有释放写锁的情况下,都无法进行操作
B线程对数据2进行写操作,在其没有释放锁的时候,其他线程也无法进行操作
如果A线程在对数据1进行写操作的时候,还想对数据2进行操作,那么线程A就要等到线程B释放锁才行
恰巧的是线程B也想对数据1进行操作,那么它也要等待线程A释放锁,线程A,B都在等待对方释放锁,然后就会发生死锁
2.案例
/**
*通过向map里面放数据和取数据,模拟读写锁的操作
* 写锁是独占锁,读锁是共享锁
*/
class myCha{
//创建一个map
private volatile Map<String,String> map = new HashMap<>();
//创建一个读写锁对象
private ReadWriteLock lock = new ReentrantReadWriteLock();
//写操作
public void put(String key,String value){
//写操作 上锁
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"正在进行写操作");
//暂停一会
TimeUnit.MILLISECONDS.sleep(300);
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"写完了");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.writeLock().unlock();
}
}
//读操作
public String read(String key){
lock.readLock().lock();
System.out.println(Thread.currentThread().getName()+"正在进行读操作");
try {
//暂停一会
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.readLock().unlock();
}
System.out.println(Thread.currentThread().getName()+"读完了");
return map.get(key);
}
}
public class Thead {
public static void main(String[] args){
myCha myCha = new myCha();
//创建5个写锁的线程
for (int i = 1; i < 6; i++) {
final int num = i;
new Thread(()->{
myCha.put(num+"",num+"");
},String.valueOf(i)).start();
}
//创建5个读锁的线程
for (int i = 1; i < 6; i++) {
final int num = i;
new Thread(()->{
myCha.read(num+"");
},String.valueOf(i)).start();
}
}
}
执行结果
读写锁特性
1.在写的时候可以进行读,也称锁降级,但是读的时候不能写
例子
/**
* 读写锁降级的例子
* 在写的时候可以进行读,也称锁降级,但是读的时候不能写
*/
public class Thead {
public static void main(String[] args) {
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
//读锁
ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
//写锁
ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
//写锁上锁
writeLock.lock();
System.out.println("写");
//读锁上锁
readLock.lock();
System.out.println("读");
//写锁释放
writeLock.unlock();
//读锁释放
readLock.unlock();
}
}
代码运行结果
如果是反过来呢?
代码运行结果
阻塞队列
1.概念
线程1往队列里面放元素,线程2往里面取元素,如果线程1放元素的时候,队列满了,就会发生阻塞,同理,线程2取队列里面的元素,如果此时队列没有元素了,线程2也会发生阻塞的情况,直到队列里面有新的元素
2.用法
首先先创建一个阻塞队列,阻塞队列里面有4组放和取的方法,每组方法都有些差异。
下面演示的是不一样的地方,正常的取和放效果都是一样的
//创建一个阻塞队列 设置阻塞队列数组长度为3 BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
第一组(add放和remove取)
执行结果
第二组(offer放和poll取)
执行结果
第三组(put放和take取)
执行结果
第4组(offer和poll)
执行结果
线程池相关笔记
线程池 单独整理了一篇