线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。
进程是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资源的基本单位。
每个进程都有自己的地址空间,即进程空间,在网络或多用户换机下,一个服务器通常需要接收大量不确定数量用户的并发请求,为每一个请求都创建一个进程显然行不通(系统开销大响应用户请求效率低),因此操作系统中线程概念被引进。
(1)地址空间:同一个进程的线程共享本线程的地址空间,而进程之间的地址空间相互独立。
(2)资源拥有:同一个进程的线程共享本线程的资源,如系统内存,I/O,cpu等,但进程之间的资源相互独立
(3)一个进程崩溃后,不会对其他进程造成影响;但一个线程崩溃后,整个进程都要崩溃,因此进程的健壮性比线程好。
(4)系统调度时,切换进程消耗的资源大,效率高;而切换线程的消耗很小,因此线程适用于频繁切换的过程。
(5)如果要求并发执行并且又要共享某些变量的操作,只能使用线程而不能使用进程。
(6)每个进程都有一个程序运行的入口,顺序执行的序列和程序入口,可独立执行,而线程不能独立执行,它必须依赖它所属的进程。
(1)继承Thread类,重写run方法
(2)实现Runnable接口,重写run方法
(3)实现Callable接口,实现call方法
区别:
①前两种方式创建的线程的方法体均是run方法,而第三种实现Callable接口创建的线程的方法体是call方法。
②实现Callable接口创建的线程可以声明抛出异常,而前两种不可以。
③实现Callable接口创建的线程的执行体,即call方法可以有返回值,可用用Future接口(实现类为futureTask)接收,用futue接口的get方法获取(会阻塞),而前两种方式创建的线程的run方法则不能有返回值。
④使用实现接口创建的线程还可以继承其他类,继承Thread类创建的线程则不行。
Start方法是开启一个线程,真正实现多线程并发运行。而run方法是一个普通方法,调用执行run方法时,不会交替执行,会把run方法的方法体执行完毕后,再去调度下一个线程。
多线程就是分时利用CPU,宏观上让所有线程一起执行 ,也叫并发。
概念:CyclicBarrie是栅栏锁,栅栏类似于闭锁,它能阻塞一组线程直到某个事件的发生。栅栏与闭锁的关键区别在于,所有的线程必须同时到达栅栏位置,才能继续执行。闭锁用于等待事件,而栅栏用于等待其他线程。
CyclicBarrier可以使一定数量的线程反复地在栅栏位置处汇集。当线程到达栅栏位置时将调用await方法,这个方法将阻塞直到所有线程都到达栅栏位置。如果所有线程都到达栅栏位置,那么栅栏将打开,此时所有的线程都将被释放,而栅栏将被重置以便下次使用。
应用程序实例:
public class CyclicBarrieThread extends Thread {
private CyclicBarrier c;
public CyclicBarrieThread(CyclicBarrier c) {
this.c = c;
}
@Override
public void run() {
super.run();
System.out.println(Thread.currentThread().getName() + "到达栅栏,开始等待");
try {
c.await();
System.out.println(Thread.currentThread().getName() + "等待完毕,开始执行");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
// main方法
public class CyclicBarrieTest {
public static void main(String[] args) {
CyclicBarrier c = new CyclicBarrier(3);
for (int i = 0; i < 3; i++) {
CyclicBarrieThread cyclicBarrieThread = new CyclicBarrieThread(c);
cyclicBarrieThread.start();
}
}
}
CountDownLatch类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。
应用程序实例:
public class CountDownLatchTread implements Runnable{
private CountDownLatch c;
public CountDownLatchTread(CountDownLatch c) {
this.c = c;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在执行");
try {
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + "执行完毕");
c.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// main方法
public class CountDownLatchTest {
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
CountDownLatchTread c = new CountDownLatchTread(countDownLatch);
Thread thread = new Thread(c);
thread.start();
}
System.out.println("等待三个线程执行完毕~~~~~~~~~~~~");
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("两个线程执行完毕,主线程开始执行");
}
}
CountDownLatch | CyclicBarrier |
---|---|
减计数方式 | 加计数方式 |
计算为0时释放所有等待的线程 | 计数达到指定值时释放所有等待线程 |
计数为0时,无法重置 | 计数达到指定值时,计数置为0重新开始 |
调用countDown()方法计数减一,调用await()方法只进行阻塞,对计数没任何影响 | 调用await()方法计数加1,若加1后的值不等于构造方法的值,则线程阻塞 |
不可重复利用 | 可重复利用 |
(1)CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置,可以使用多次,所以 CyclicBarrier能够处理更为复杂的场景;
(2)CyclicBarrier还提供了一些其他有用的方法,比如getNumberWaiting()方法可以获得CyclicBarrier阻塞的线程数量,isBroken()方法用来了解阻塞的线程是否被中断
(3)CountDownLatch允许一个或多个线程等待一组事件的产生,而CyclicBarrier用于等待其他线程运行到栅栏位置。
定义:
(1)如果一个操作happens-before另一个操作,那么第一个操作的操作结果要对第二个操作可见,而且第一个操作要优先于第二个操作执行。
(2)两个操作如果有happens-before关系,并不意味着两个操作一定要按照happens-before原则指定的规则顺序执行,如果指令重排序并不影响最终的操作结果,那么指令重排序并不非法。
规则:
(1)程序次序规则:一个线程内,根据代码的顺序,写在前面的操作优先发生于写在后面的操作。
(2)锁定规则:一个unlock操作先行于对同一个锁的lock操作。
(3)Volite变量规则:对一个变量的写操作先行于后面对这个对象的读操作。
(4)传递规则:如果A操作先行于B操作,B操作先行于C操作,那么A操作先行于C操作。
(5)线程启动规则:一个Thread的start方法先行于此线程的每一个动作。
(6)线程中断规则:一个Thread的interrupt方法先行于被中断线程代码检测到中断事件的发生
(7)线程终结规则:一个Thread的所有操作先行于此线程被终结检测。
(8)对象终结规则:一个对象的初始化操作先行于对这个对象finalize操作。
Volatile修饰变量保证了变量的可见性,即在一个线程对其进行修改之后,其他线程再去查看这个变量时一定看到的是第一个线程修改后的结果。
Volatile修饰变量保证了指令的有序性,即被Volatile修饰的变量的声明语句或赋值语句禁止指令重排序,但是只保证有volatile关键字的语句之前的模块在它之前执行,有volatile关键字的语句之后的模块在它之后执行,而不保证模块内部指令的有序性。
Volatile修饰变量不保证原子性,即非原子操作仍会存在线程安全问题。如volatile int a = 0;
a++; 这里的++操作即为非原子操作。
当代码块被sychronized修饰时,当一个线程获得了对应的锁,并执行改代码块时,其他线程无法获得锁,并且处于等待状态,直到获取锁的线程释放了锁,在以下两种情况下可释放锁:
(1)代码块正常运行完毕
(2)线程出现异常
如果一个线程获取了锁,但是这个线程又因为其他原因如等待I/O或者sleep处于阻塞状态,那么其他线程只能继续等待,影响程序的执行效率。
除此之外,如果代码块被Synchronized修饰,那么相对于读写操作而言是不合理的,线程在进行读操作的时候也会使其他想要进行读操作的线程等待。
(1)Lock必须手动释放锁,如果未释放锁则会出现死锁的情况,而Synchronized则在代码执行完毕或者异常终止时自动释放锁。
(2)Synchronized是java的一个关键字,synchronized是内置的语言实现,而lock是一个 接口
(3)在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常 激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。
(4)Lock可以响应中断,Synchronized不能响应中断
(5)Lock可以是公平锁,也可以是非公平锁,且它是可重入锁,Synchronized是非公平 锁,也可以重入
(6)Lock可调用tryLock方法查看获取锁的状态,如果获取到锁,则立即返回false,不 会使线程处于等待状态,而Synchronized会使未获得锁的线程处于等待状态。
(1)调用lockInterruptibly()方法时,如果当前线程获取不到锁,那么就会调用当前线程的interrupt方法中断它的阻塞过程。应该注意的是,interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。
(2)tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true
ReadWriteLock是一个接口,其中定义了两个方法,writeLock和readLock,一个用于获取读锁,一个用于获取写锁。读锁保证了所有的线程在进行读操作是可以并发执行,但是如果此时线程要获取写锁,就必须等待当前线程的读锁释放。同样的,如果一个线程获取到写锁,其他线程想要获取读锁和写锁进行读写操作时,必须等待当前线程的写锁释放。
(1)可重入锁:具有可重入性的锁,即一个同步方法中调用另一个同步方法,不需要重 新获得锁,synchronized和Lock都具备可重入性,都为可重入锁。
(2)可中断锁:可以响应中断的锁,synchronized是不可中断,Lock是可中断锁
(3)公平锁:尽量按照请求锁的顺序来获取锁,当一个锁被释放时,等待队列队首(等 待时间最久的线程)优先获得锁。非公平锁无法保证获取锁的顺序是按照 请求锁的顺序进行的,这可能导致某些线程永远得不到锁。Synchronized 是非公平锁,而ReentrantLock和ReentrantReadWriteLock,它默认情况下 是非公平锁,但是可以设置为公平锁。
(4)读写锁:读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一 个写锁,正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。 ReadWriteLock就是读写锁,它是一个接口, ReentrantReadWriteLock实现了这个接口。可以通过readLock()获 取读锁,通过writeLock()获取写锁。
AQS是大多数同步组件的核心部分,它主要实现的是对同步状态的管理,对阻塞的线程进行排队以及等待和唤醒线程等一些底层的实现处理。
AQS的核心也包括同步队列,独占式锁的获取和释放,共享锁的获取和释放以及可中断锁,超时等待锁获取这些特性的实现。
同步队列(FIFO算法):AQS的同步队列是用双向双端链表实现的,它的结点有以下属性:
volatile int waitStatus //节点状态
volatile Node prev //当前节点/线程的前驱节点
volatile Node next; //当前节点/线程的后继节点
volatile Thread thread;//加入同步队列的线程引用
Node nextWaiter;//等待队列中的下一个节点
其中节点的状态有以下几种:
int CANCELLED = 1//节点从同步队列中取消
int SIGNAL = -1//后继节点的线程处于等待状态,如果当前节点释放同步状态会通知后继节点,使得后继节点的线程能够运行;
int CONDITION = -2//当前节点进入等待队列中
int PROPAGATE = -3//表示下一次共享式同步状态获取将会无条件传播下去
int INITIAL = 0;//初始状态
同步队列的出对和如对操作对应着两个操作:获取锁失败的线程的入队和获取锁成功的线程的出队操作。
获取独占锁失败后,线程会封装成一个同步队列的Node,然后判断同步队列的尾指针是否为空,如果为空,则执行enq方法,如果不为空,则使用尾插法插入节点,如果插入成功,返回此节点,插入失败,执行enq方法。
从上面获取锁失败的线程入队的执行过程我们可以推测出enq方法的任务:
(1)如果尾节点为空,即当前同步队列为空,则当前线程为第一个进入同步队列的线程, 因此enq方法要为同步队列创建头节点。
(2)如果尾节点不为空,说明CAS尾插插入封装线程的节点失败,因此需要负责自旋 进行尝试CAS尾插,直到节点插入成功为止。
总结:
(1)线程获取锁失败,线程被封装成Node进行入队操作,核心方法在于addWaiter()和enq(),同时enq()完成对同步队列的头结点初始化工作以及CAS操作失败的重试;
(2)线程获取锁是一个自旋的过程,当且仅当 当前节点的前驱节点是头结点并且成功获得同步状态时,节点出队即该节点引用的线程获得锁,否则,当不满足条件时就会调用LookSupport.park()方法使得线程阻塞;
(3)释放锁的时候会唤醒后继节点;
摘选:https://www.jianshu.com/p/cc308d82cc71
可中断式获取锁(acquireInterruptibly方法):与获取独占锁的思路几乎完全一样,唯一的区别是当可中断式锁获取不到时,不会阻塞,会中断当前线程,抛出中断异常。
超时等待获取锁(tryAcquireNanos()方法):流程如下
共享锁的获取(acquireShared()方法):在该方法中会首先调用tryAcquireShared方法,tryAcquireShared返回值是一个int类型,当返回值为大于等于0的时候方法结束说明获得成功获取锁,否则,表明获取同步状态失败即所引用的线程获取锁失败,会执行doAcquireShared方法,它的逻辑几乎和独占式锁的获取一模一样,这里的自旋过程中能够退出的条件是当前节点的前驱节点是头结点并且tryAcquireShared(arg)返回值大于等于0即能成功获得同步状态。
(1)共享变量
(2)Wait,notify,notifyAll方法
(3)Lock/Condition机制
(4)管道通信,创建管道输出流PipedOutputStream pos和管道输入流PipedInputStream pis,将pos和pis匹配,pos.connect(pis),将pos赋给信息输入线程,pis赋给信息获取线程,就可以实现线程间的通讯了.
下面主要介绍Lock/Condition机制。
Object的wait和notify/notify是与对象监视器配合完成线程间的等待/通知机制,而Condition与Lock配合完成等待通知机制,前者是java底层级别的,后者是语言级别的,具有更高的可控制性和扩展性。
Condition是AQS的一个内部类,它类似于AQS,但两者又有差异。AQS是一个双端双向队列,而Condition是一个单向的队列,而且一个Lock可以调用newCondition方法创建多个等待队列,而同步队列只能有一个(对象Object对象监视器上只能拥有一个同步队列和一个等待队列)。因为Condition是AQS的内部类,因此它复用了AQS的节点。
当前线程调用condition.await()方法后,会使得当前线程释放lock然后加入到等待队列中,直至被signal/signalAll后会使得当前线程从等待队列中移至到同步队列中去,直到获得了lock后才会从await方法返回,或者在等待时被中断会做中断处理。
管道流虽然使用起来方便,但是也有一些缺点
1)管道流只能在两个线程之间传递数据
线程consumer1和consumer2同时从pis中read数据,当线程producer往管道流中写入一段数据后,每一个时刻只有一个线程能获取到数据,并不是两个线程都能获取到producer发送来的数据,因此一个管道流只能用于两个线程间的通讯。不仅仅是管道流,其他IO方式都是一对一传输。
2)管道流只能实现单向发送,如果要两个线程之间互通讯,则需要两个管道流.
(1)管道(Pipe):管道可用于具有亲缘关系进程间的通信,允许一个进程和另一个与它有共同祖先的进程之间进行通信。
(2)命名管道(named pipe):命名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关 系 进程间的通信。命名管道在文件系统中有对应的文件名。命名管道通过命令mkfifo或系统调用mkfifo来创建。
(3)信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送 信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数)。
(4)消息(Message)队列:消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺
(5)共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
(6)内存映射(mapped memory):内存映射允许任何多个进程间通信,每一个使用该机制的进程通过把一个共享的文件映射到自己的进程地址空间来实现它。
(7)信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。
(8)套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。
相同点:Notify和notifyAll都能唤醒线程,并且只能有一个线程获得锁。
不同点:notify是唤醒一个线程,notifyAll是唤醒所有的线程,使所有的线程竞争获取资源对象的锁,并且只能由一个线程获得锁,执行代码。
注意:wait()方法并不是在等待资源的锁,而是在等待被唤醒(notify()),一旦被唤醒后,被唤醒的线程就具备了资源锁(因为无需竞争),直至再次执行wait()方法或者synchronized代码块执行完毕。
发生死锁的四个条件:互斥资源、循环等待、不可剥夺、请求与保持
避免死锁的方法:避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序(升序或降序)做操作来避免死锁。
死锁和活锁的区别:活锁和死锁类似,不同之处在于处于活锁的线程或进程的状态是不断改变的,活锁可以认为是一种特殊的饥饿。一个现实的活锁例子是两个人在狭小的走廊碰到,两个人都试着避让对方好让彼此通过,但是因为避让的方向都一样导致最后谁都不能通过走廊。简单的说就是,活锁和死锁的主要区别是前者进程的状态可以改变但是却不能继续执行。
yield方法可以让当前的线程暂停,但是不会使当前线程阻塞,只是将当前线程由运行状态变成就绪状态,系统进行下一次调度时,此线程仍可获得CPU的执行权。实际上,当某个线程调用了yield()方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态的线程才会获得执行的机会。
关于sleep和yield的区别
(1)sleep方法会给其他线程执行机会,与线程的优先级无关,而yield会给与当前线程优先级相同的或者优先级更高的线程执行机会。
(2)Sleep方法会使线程由运行状态转化为阻塞状态,直到阻塞时间结束,而yield会使线程由运行态转化为就绪状态,不会使线程阻塞
(3)Sleep方法的方法声明上抛出了InterruptException,因此在调用时需要抛出或者捕获异常,yield则没有抛出异常。
(4)Sleep方法比yield方法具有更好的一致性,不建议使用yield方法控制并发线程的执行
(1)返回值不同,excute方法不关心返回值,他只执行线程,而submit方法由返回值(Future)。
(2)对异常的处理不同,excute方法会抛出异常,而submit方法不会抛出异常,除非调用Futrue的get方法
不可变对象可以在没有同步的情况下共享,降低了对该对象进行并发访问时的同步化开销。可是Java没有@Immutable这个注解符
要创建不可变类要实现下面几个步骤:
通过构造方法初始化所有成员、对变量不要提供setter方法、将所有的成员声明为私有的,这样就不允许直接访问这些成员、在getter方法中,不要直接返回对象本身,而是克隆对象,并返回对象的拷贝。
public class SingletonDoubleLock {
private volatile static SingletonDoubleLock singletonDoubleLock;
private SingletonDoubleLock() {
}
public static SingletonDoubleLock getInstance(){
if (singletonDoubleLock == null) {
synchronized (SingletonDoubleLock.class) {
if (singletonDoubleLock == null) {
singletonDoubleLock = new SingletonDoubleLock();
}
}
}
return singletonDoubleLock;
双重检查锁定的问题是:并不能保证它会在单处理器或多处理器计算机上顺利运行。
java中的wait和sleep方法都会引起线程的暂停,他们适用于不同的场景。Sleep方法使Thread类的静态方法,调用此方法会使线程休眠一段时间,此时线程会进入阻塞状态,但不会释放锁,待休眠时间结束够回到就绪态。而wait方法使Object的方法,调用wait方法会使当前线程阻塞,并且释放锁,当且仅当其他线程调用notify或者notifyAll方法时才会重新进入就绪态。
(1)给线程起一个有实际意义的名字
(2)避免锁定和缩小同步的范围
(3)多用同步类少用wait和notify
(4)多用并发集合少用同步集合