1. java的JMM 中的原子性、可见性、有序性
(1) 原子性 是指一个操作是不可中断的 。即使是多个线程一起执行的时候,一个线程一旦开始,就不会被其他线程干扰。 保证原子性 就是线程运行 不会被其他线程干扰 ,该线程中的内容也不被其他线程所影响 修改。
(2) 可见性 就是指 当一个线程修改了某一个共享变量的值,其他线程就能立即知道该变量被修改了。 在多线程中 全局变量可能 将变量值缓存在cache 中或者在寄存器中那么,某个修成修改了值 ,可能其他线程不能立即知道 该变量的修改。那么其他线程就会出现问题。
(3) 有序性 如果在串行上来说,有序性会按照顺序执行,但是在多线程中可能会出现执行 的时候 进行 指令重排的情况 。串行指令重排能导致语义一致,但是多线程的时候可能保证不了了
2. 指令不能进行重排 : Happen-Before 规则
转载自:https://www.jianshu.com/p/8897d8c651de
3. 进程的概念
(1) 进程:官方的语言是计算机中的程序,关于某数据集合上的一次运行活动。使系统进行资源分配和调度的基本单位单位,是操作系统的基本单位,是操作系统结构的基础。在现在的线程设计的计算机结构中,进程是线程的容器,程序是指令数据及组织形式的描述,进程是程序的实体,但是总的来说 进程是线程的容器。在平常我们也会说线程是轻量级的进程,是程序执行的最小单位。使用多线程而不是用多进程进行并发设计的原因 线程间的切换与调度的成本远小于进程
(2) 线程的生命周期
线程 状态分为 NEW(新创建的),RUNNABLE(运行中),BLOCKED(同步块阻塞),WAITING(等待无限时间的等待),TIMED_WAITING(等待有限的时间 ),TERMINATED(结束) 六种。
(4) 线程
(1) 线程的建立
Thread thread =new Thread();
thread.start();
//在这里启动后 会找到我们的run方法 去执行
(2) 终止线程
使用Thread.stop 该方法停止线程。但是该方法会直接终止线程,并且将立即释放这个线程所持有的锁,而这些锁是用来维持对象的唯一性的。如果数据写入到一半 锁时候后,另外一个线程 就会读取到该对象,但是这就读取到了一个不一致的对象这样就会造成程序出现问题。
从图中我们可知 对象有两个属性 分别为ID 和NAME , 条件为当ID和NAME 相等 时 对象是一致的,否则表示对象出错。 我们的写入线程总是把 ID和NAME 写成相同的值。 然后当我们在写线程把ID写入到对象中的时候 ,突然执行stop 操作,那么该对象的ID就会变成1 而NAME仍然为别的数值,这样就处于不一致的情况,写线程在这个时候把锁释放后,读线程争取到锁,开始读取数据,这样就读取到了错误数据。
(3) 线程中断
概念 :让目标线程停止执行,但是是高知目标线程希望线退出,具体退出由目标线程自己决定。
相关的方法,暂时只介绍Thread的方法
Thread.interrupt() //中断线程 也是告知目标线程中断,也就是设置中断标志位
Thread.isInterrupted() //判断是否被中断–通过上面方法设置的中断标志位来判断是否被中断
Thread.interrupted //判断是否被中断,并清楚中断状态,,, 用来判断当前线程的中断状态,同事清除中断标志位状态。
1) 程序实现
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(){
@Override
public void run() {
while(true){
//在这里判断 是否有中断位 ,又中断位了就将线程终止
if(Thread.currentThread().isInterrupted()){
System.out.println("中断了");
break;
}
Thread.yield();
}
}
};
t1.start();
Thread.sleep(2000);
t1.interrupt(); // 在这里进行通知中断
}
(4) 通知(notity)与等待(wait)
- 当一个线程执行wait方法的时候,那么当前线程就会停止继续执行,转为等待的状态,然后等其他线程执行notity 方法为止。执行如图所示,如果用notity是随机唤醒一个线程不是按照执行顺序唤醒线程。
- wait 执行后会释放对应的监视器,其他线程会获得该监视器,然后执行notity 之后 会释放监视器,wait 重新获得监视器,继续执行程序。
public class SimpleWN {
final static Object object =new Object();
public static class T1 extends Thread{
@Override
public void run() {
synchronized(object) {
System.out.println(System.currentTimeMillis()+ "T1 start!");
try {
System.out.println(System.currentTimeMillis() + "T1 wait for object");
object.wait(); //wait等待 然后等到notity来唤醒 ,但是notity唤醒也是随机的 并且wait 方法会释放目标对象的锁而下面的sleep方法就不会释放
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis() + ": T1 end!");
}
}
}
public static class T2 extends Thread{
@Override
public void run() {
synchronized (object) {
System.out.println(System.currentTimeMillis()+"T2 start ! notity one thread");
object.notify();
System.out.println(System.currentTimeMillis()+"T2 end!");
try {
Thread.sleep(2000); //睡眠2秒
} catch (Exception e) {
// TODO: handle exception
}
}
}
}
public static void main(String[] args) {
Thread thread1=new T1();
Thread thread2=new T2();
thread1.start();
thread2.start();
}
}
(5)挂起(suspend)和继续执行(resume)
- 这是被废弃的方法,说明下原理:suspend 在导致线程暂停的通识不把锁的资源释放,其他线程 如果访问没有被释放的锁,那么也会受到牵连导致无法 继续执行。除非在该暂停的线程上进行了resume操作那么该线程才会继续执行,阻塞的其他线程也会继续执行。如果resume 操作在suspend前执行了那么被执行suspend的线程很难有机会再被继续执行。这种情况下很可能出现死锁的情况
(6)等待线程结束(join)和谦让(yield)
- 我们在程序的执行中,很可能会出现多个线程之间有依赖关系的线程,只有当前一个线程执行完毕后才能继续执行以后的线程,那么我们可以使用Thread.join方法来实现 ,等待线程结束。看如下代码
- yield方法:谦让,谦让,意思 当该方法执行后会让当前线程把cpu 让出来,到那时有一点需要注意的是让出cpu后该线程还会参与到cpu的争夺中,会不会分配到 这就不一定了
public class JoinMain {
/**
* volatile 变量的可见性 不可代替锁
*/
private volatile static int i=0;
public static class AddThread extends Thread{
@Override
public void run() {
for(i=0;i<10000000;i++);
}
}
public static void main(String[] args) throws Exception {
AddThread thread= new AddThread();
thread.start();
thread.join(); //等待的意思,本质是让调用线程wait 在当前线程实例上调用的是wait(0) 这个方法
System.out.println(i);
}
}
代码执行结果如图:
运行结果:
不用join方法的话,结果如下图:
运行结果:
从上面两个图可以看出来 如果不适用join 那么执行的结果会很小。执行join方法后会让该线程等待addThread线程执行完毕。
(7)volatile 与java内存模型(JMM)
- java的内存模型都是围绕着原子性、有序性、还有可见性来展开的。
- volatile 主要是用来告知虚拟机,被volatile 修饰的变量要注意,不要随意改动优化目标指令,使用该关键字是为了保证变量修改后会通知所有线程能看到该改动。保证变量的可见性。 volatile 并不能代表锁,无法保证复合操作的原子性。但是volatile 能保证元素的可见性和在一定程度上保证元素的有序性。保证修改的值会立即被更新到主存。当有其他线程需要读取时,它会去内存中读取新值。 volatitle 能保证一些的是禁止指令重排。
(8)线程组 - 在系统中我们可以把属于相同功能的线程放到一个线程组里面。
- 关键字:ThreadGroup
public class ThreadGroupName implements Runnable {
@Override
public void run() {
String groupName = Thread.currentThread().getThreadGroup().getName()
+"-"+Thread.currentThread().getName();
while(true) {
System.out.println("I am "+groupName);
try {
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ThreadGroup group= new ThreadGroup("PrintGroup");
Thread t1=new Thread(group,new ThreadGroupName(),"T1");
Thread t2 =new Thread(group,new ThreadGroupName(),"T2");
t1.start();
t2.start();
System.out.println(group.activeCount());
group.list();
}
}
(9) 守护线程
- 守护线程是在后台默默运行的一些系统性的服务 ,常见的守护线程有垃圾回收线程,JIT线程。 与之对应的线程就是 用户线程也可以被认为是系统的工作线程。 用户线程执行完毕后就会结束。在java中 只有守护线程的时候 java虚拟机就会自然的退出。
关键字:thread.setDaemon(true); //设置为守护线程
public class DaemonDemo {
public static class DaemonT extends Thread {
@Override
public void run() {
while(true) {
System.out.println("I am alive");
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
Thread thread= new DaemonT();
thread.setDaemon(true); //设置为守护线程
thread.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
(10)线程优先级
- 线程中有自己的优先级,优先级别高的线程 在争夺资源 的时候 处于优势的状态,也有可能出现抢占资源失败的情况
- 如下面代码所示,线程中有优先级别设置,在java 中使用1到10 表示线程有限级别,我们一般用如图所示的变量来标识 线程优先级
- 关键字:thread.setPriority(Thread.MAX_PRIORITY);
public class PriorityDemo {
public static class HightPriority extends Thread{
static int count =0;
@Override
public void run() {
while(true) {
synchronized (PriorityDemo.class) {
count++;
if(count>1000000) {
System.out.println("HightPriority is complete");
break;
}
}
}
}
}
public static class LowPriority extends Thread{
static int count =0;
@Override
public void run() {
while(true) {
synchronized (PriorityDemo.class) {
count++;
if(count>1000000) {
System.out.println("LowPriority is complete");
break;
}
}
}
}
}
public static void main(String[] args) {
Thread high =new HightPriority();
LowPriority lowPriority=new LowPriority();
high.setPriority(Thread.MAX_PRIORITY); //在这里设置 线程的优先级
lowPriority.setPriority(Thread.MIN_PRIORITY);
lowPriority.start();
high.start();
}
}
5. 线程安全
(1)synchronized
- 多线程在处理的过程中如果我们没有给程序进行过任何处理的话,在执行的过程中可能会出现多个线程对同一个数据同时进行修改,那么就可能会出现就修改一次的情况。比如程序中有i++,这个操作那么在执行过程中可能就会出现修改一次的情况。
为了防止多线程操作出现问题,我们必须保证多个线程对线程不安全的数据访问完全同步。在Java 中提供了一个关键字synchronized来实现这个功能。 - 关键字synchronized 的作用就是实现线程之间的同步,对需要同步的代码加锁,实现 只有一个线程进入同步代码块中。来保证线程间的安全性。大概有以下几种用法
1)指定加锁对象,就是利用指定的对象在进入代码块之前获得 该对象。
2)直接作用于实例方法上:利用当前的实例进行加锁,new 之后的对象
3)直接作用于静态方法:对当前类加锁。 类变量。
(2)并发下的线程安全 - 如果程序中本来数据进行的都是正数操作,预期都是正数的话,那么如果出现负数,那么可能出现这个情况,可能就是内存溢出导致的问题。比如long 型 如果超过最大值那么就会变成负数。 这就是内存溢出的问题。线上有时候就会碰到该问题。需要注意!
并发下的ArrayList
static ArrayList<Integer> al=new ArrayList<Integer>(10);
public static class AddThread implements Runnable{
public void run() {
for(int i= 0; i<1000000; i++) {
al.add(i);
}
}
}
public static void main(String[] args) throws Exception {
Thread t1 =new Thread(new AddThread());
Thread t2 =new Thread(new AddThread());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(al.size());
}
}
这个代码执行完毕后,可能出现三种结果:
- 正常结束,大小为200000
- 出现异常的情况。下标越界情况
这是因为ArrayList 在扩容的时候 ,内部的一致性被破坏掉。因为ArrayList 是线程不安全的,没有锁机制,导致在多个线程访问的时候 出现了不一致的内部状态,导致出现下标越界
- 出现一个隐蔽的错误就是没有出现下标的越界的情况 ,只给了一个大小的值。出现这个情况的原因是多个线程在进行赋值的时候,对同一个位置进行赋值,在这种情况下就没有错误提示。我们可以使用Vector 代替ArrayList
并发下的hashMap
- hashMap 同样也不是线程安全的 。会出现如下结果
1)程序正常运行结果也符合预期。
2)程序正常结束,但是不符合预期,小于我们需要的数据大小
3)程序永远不会结束 - 在验证过程中我们可以使用jps 命令来查看当前系统中java 的进程。如果想看具体 使用 可以使用 jps -help 有命令介绍。 这个是在linux上使用 一般会用jps -ml 显示内容和名称。
- 在使用jps 后我们可以看到端口号 。然后使用jstack +端口号来看我们的运行的线程
- java7线面的源码hashMap ,java8现在已经不是该循环
- 解决方案可以使用ConCurrentHashMap代替
(3)错误的加锁问题
- 在使用加锁的时候。我们不要使用锁对象是Integer ,String 这种对象去作为一个锁,可能造成加锁没有加成功的时候,因为如果我们在其他地方对其修改后,该对象的地址就会变成新的对象。那么锁就会失去作用
(4)同步控制
- synchronized 扩展:重入锁
1)重入锁来代替synchronized,在Jdk1.6以后 synchronized的性能与重入锁性能差不多
2)重入锁的实现(ReenterLock.lock)
public static ReentrantLock lookLock= new ReentrantLock();
public static int i=0;
public void run() {
for(int j=0; j<100000;j++) {
lookLock.lock(); //加锁
lookLock.lock();
try {
i++;
} catch (Exception e) {
e.printStackTrace();
}finally {
lookLock.unlock();
lookLock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
ReenterLock reenterLock=new ReenterLock();
Thread t1=new Thread(reenterLock);
Thread t2=new Thread(reenterLock);
t1.start(); t2.start();
t1.join();t2.join();
System.out.println(i);
}
- 在上述代码中我们使用了重入锁来保护临界区资源i,确保程序的操作的安全性。我们在使用重入锁的时候需要显示的指定何时加锁,何时释放锁。必须释放锁不然其他线程没有机会访问临界区了。
- 我们在代码上还实现了多次加锁的控制,同一个线程可以加入多个锁来控制 ,但是释放的时候加了 几个锁就要释放几个锁。不然其他线程也无法进入临界区。
(5) 中断响应 - 我们在使用synchronized 来加锁的话,那么结果只有两种可能 一是获得锁继续执行。二是 继续等待。但是我们使用重入锁就可以使其中断。
- 锁申请等待限时
- 除了我们在等待外部通知之外,避免死锁还有另外一种方法。就是限时等待。我们可以给定一个等待的时候后。如果线程很长时间拿不到锁,等待时间到了那么让其自动放弃.
public class TryLock implements Runnable {
public static ReentrantLock lock1 =new ReentrantLock();
public static ReentrantLock lock2 =new ReentrantLock();
int lock;
public TryLock(int lock) {
this.lock=lock;
}
public void run() {
if(lock==1) {
while(true) {
if(lock1.tryLock()) {
try {
try {
Thread.sleep(500);
} catch (Exception e) {
}
if(lock2.tryLock()) {
try {
System.out.println(Thread.currentThread().getId() +":My Job done");
return ;
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
}
}
else {
while(true) {
if(lock2.tryLock()) {
try {
try {
Thread.sleep(500);
} catch (Exception e) {
}
if(lock1.tryLock()) {
try {
System.out.println(Thread.currentThread().getId() +"My Job done");
return ;
} finally {
lock1.unlock();
}
}
} finally {
lock2.unlock();
}
}
}
}
}
public static void main(String[] args) {
TryLock r1=new TryLock(1);
TryLock r2=new TryLock(2);
Thread t1 =new Thread(r1);
Thread t2 =new Thread(r2);
t1.start();
t2.start();
}
}
(6) 公平锁
- 大多数情况下,锁的申请都是非公平的。如果我们使用的是synchronized 来加锁,产生的锁就是非公平的。但是我们可以使用重入锁允许我们对其公平性进行设置。 方法ReentrantLock(boolean fair); 当 fair 为true 时,代表锁是公平的,但是使用公平锁 需要维护一个有序的队列,就会造成性能降低。因此我们在默认情况下不使用公平锁。
- 整理ReentrantLock 的几个方法如下:
1)lock():获得锁 ,吐过锁被占用 ,则等待;
2)lockInterruptibly(): 获得锁,单会优先响应中断;
3)tryLock(); 尝试获得锁,成功返回true,失败返回false;不等待立即返回。
4)tryLock(long time,TimeUnit unit): 给定时间内获得锁。
5)unlock(); 释放锁。
(7) 在重入锁实现中 主要包含三个元素:
- 使用原子状态,原子操作使用CAS操作来存储当前锁的状态,判断锁是否被别的线程持有。
- 等待队列。所有没有请求到锁的线程,会被放入等待队列中进行等待,待有线程释放后 系统就能从队列中唤醒一个线程,继续工作。
- 阻塞park()和unpark,用来挂起和恢复线程。没有得到锁的线程将会被挂起。
(8) 重入锁的好搭档:Condition条件
- Condition 条件与Object 的wait 和Object.notify 方法类似
- Condition 有如下基本方法:
1)await() 方法会使当前线程号等待,同时释放当前锁,当其他线程中使用signal() 或者使用signalAll()方法时,线程会重新获得锁并继续执行。或者当被中断的时候也能跳出等待。
2)waitUninterruptibly() 与await类似 但是该方法不会再等待的过程中响应中断。
3)singal() 方法用于唤醒 一个等待中的线程。singalAll唤醒所有在等待中的线程。
演示代码如下:
public class ReenterLockCondition implements Runnable {
public static ReentrantLock lock =new ReentrantLock();
public static Condition condtionCondition =lock.newCondition(); //生成一个Condition对象
public void run() {
try {
lock.lock();
condtionCondition.await(); //执行这里的时候要求有相关的重入锁,在调用之后会释放锁
System.out.println("Thread is going on");
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ReenterLockCondition t2=new ReenterLockCondition();
Thread t1=new Thread(t2);
t1.start();
Thread.sleep(2000);
lock.lock();
condtionCondition.signal(); //发出通告 ,并且要求其先获得相关锁。
lock.unlock(); //释放重入锁
}
}
//相关具体更好的代码操作可以看ArrayBlockingQueuel例子中的put方法jdk1.7的
(9)允许多个线程同时访问:信号量
- 信号量为多线程协作提供了更为强大的控制方法,广义的说是对锁的扩展。无论是内部锁synchronized还是重入锁ReentrantLock 。一次都只允许一个线程访问一个资源。而信号量可以指定多个线程同时访问某一个资源。
- 构造方法如下:
1)Semaphore(int permits);
2)Semaphore(int permits,boolean fair) ; // 第二个参数可以指定时候公平
3)在使用构造信号量的时候要指定准入的信号量的数量。同时可以申请多个许可 - 关键字:Semaphore
1)semp.acquire(); //获取信号量 每次能进来5个线程
2)semp.release(); // 如果这里信号量泄露 没有释放 那么会导致进入临界区的线程数量越来越少。直到所有的线程不能再访问。
public class SemapDemo implements Runnable {
final Semaphore semp =new Semaphore(5);
public void run() {
try {
semp.acquire(); //获取信号量 每次能进来5个线程
Thread.sleep(2000);
System.out.println(Thread.currentThread().getId() + ":done!");
semp.release(); // 如果这里信号量泄露 没有释放 那么会导致进入临界区的线程数量越来越少。直到所有的线程不能再访问
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
//开启20个线程 去访问,会发现是5个线程一组数据
ExecutorService executionn= Executors.newFixedThreadPool(20);
final SemapDemo demo =new SemapDemo();
for(int i=0;i<20;i++) {
executionn.submit(demo);
}
}
}