java多线程学习记录(二)

一、使用Callable和Future接口创建线程

具体是创建Callable接口的实现类,并实现clall()方法。并使用FutureTask类来包装Callable实现类的对象,且以此FutureTask对象作为Thread对象的target来创建线程。

public class ThreadTest {

    public static void main(String[] args) {

        Callable<Integer> myCallable = new MyCallable();    // 创建MyCallable对象
        FutureTask<Integer> ft = new FutureTask<Integer>(myCallable); //使用FutureTask来包装MyCallable对象

        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 30) {
                Thread thread = new Thread(ft);   //FutureTask对象作为Thread对象的target创建新的线程
                thread.start();                      //线程进入到就绪状态
            }
        }

        System.out.println("主线程for循环执行完毕..");
        
        try {
            int sum = ft.get();            //取得新创建的新线程中的call()方法返回的结果
            System.out.println("sum = " + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}


class MyCallable implements Callable<Integer> {
    private int i = 0;

    // 与run()方法不同的是,call()方法具有返回值
    @Override
    public Integer call() {
        int sum = 0;
        for (; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            sum += i;
        }
        return sum;
    }

}

首先,我们发现,在实现Callable接口中,此时不再是run()方法了,而是call()方法,此call()方法作为线程执行体,同时还具有返回值!在创建新的线程时,是通过FutureTask来包装MyCallable对象,同时作为了Thread对象的target。那么看下FutureTask类的定义:

 public class FutureTask<V> implements RunnableFuture<V> {
     
    //....
    
}


public interface RunnableFuture<V> extends Runnable, Future<V> {
    
   void run();
   
}

于是,我们发现FutureTask类实际上是同时实现了Runnable和Future接口,由此才使得其具有Future和Runnable双重特性。通过Runnable特性,可以作为Thread对象的target,而Future特性,使得其可以取得新创建线程中的call()方法的返回值。

执行下此程序,我们发现sum = 4950永远都是最后输出的。而“主线程for循环执行完毕…”则很可能是在子线程循环中间输出。由CPU的线程调度机制,我们知道,“主线程for循环执行完毕…”的输出时机是没有任何问题的,那么为什么sum =4950会永远最后输出呢?

原因在于通过ft.get()方法获取子线程call()方法的返回值时,当子线程此方法还未执行完毕,ft.get()方法会一直阻塞,直到call()方法执行完毕才能取到返回值。

上述主要讲解了三种常见的线程创建方式,对于线程的启动而言,都是调用线程对象的start()方法,需要特别注意的是:不能对同一线程对象两次调用start()方法。
二、锁
1、锁的分类:

自旋锁:线程状态及上下文切换消耗系统资源,当访问共享资源的时间段,频繁的上下文切换不值得,jvm实现在线程没有获得锁的时候不被挂起,转而执行空循环,循环几次之后还没有获得锁则被挂起。

阻塞锁:阻塞锁改变了线程的运行状态,让线程进入阻塞状态进行等待,当获得相应的信号(唤醒或者时间)时,才可以进入线程上网准备就绪状态,转为就绪状态的所有线程通过竞争进入运行状态。

重入锁:支持线程再次进入的锁。

读写锁:两把锁读锁和写锁,写写互斥,读写互斥,读读共享。

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次拿数据的时候都会上锁,这样别人拿数据时候就会阻塞知道拿到锁。

乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断在此期间别人有没有去更新这个数据,可以使用版本号机制,CAS算法。悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。悲观锁在Java中的使用,就是利用各种锁。乐观锁在Java中的使用,是无锁编程。

公平锁和非公平锁:公平锁是指多个线程按照申请锁的顺序来获取锁。非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。

独享锁和共享锁:独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有。

偏向锁/轻量级锁/重量级锁:这三种锁是指锁的状态,并且是针对synchronized。在Java 5通过引入锁升级的机制来实现高效synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。

偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。

轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

lock锁接口及实现
在这里插入图片描述
实现一个自己的锁(可重入):

public class MyLock implements Lock{//同一时刻只允许一个线程获得锁,其他线程等待

	private boolean isHoldLock = false;	//锁是否被持有
	private int reentryCount = 0;		//重入锁的次数
	private Thread holdThread = null;	//记录持有锁的线程
	
	//实现lock接口的lock和unlock方法
	public void lock(){
		if(isHoldLock&&holdThread != Thread.currentThread()){
			wait();
		}
		isHoldLock = true;
		holdThread = Thread.currentThread();
		reentryCount ++;
	}
	public void unlock(){
		//判断当前线程是否是持有锁的线程,是则重入次数减一,不是就不做处理
		if(Thread.currentThread() == holdThread){
			reetryCount--;
			if(reenterCount == 0){
				notify();
				isHoldLock = false;
			}
		}
		
	}
	.........
}
public class ReentryDemo{
	public Lock lock = new MyLock();
	public void methodA(){
		lock.lock();
		System.out.println("A方法执行了");
		methodB();
		lock.unlock();
	}
	public void methodB(){
		lock.lock;
		System.out.println("B方法执行了");
		lock.unlock();
	}
	public static void main(String[] args){
		ReentryDemo demo = new ReentryDemo ();
		demo.methodA();
	}
}

在这里插入图片描述
AQS 原理以及 AQS 同步组件总结

AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。
在这里插入图片描述
详细参见:
https://blog.csdn.net/qq_34337272/article/details/83655291
https://blog.csdn.net/m_xiaoer/article/details/73459444

synchronized与Lock的区别与使用

这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。

synchronized为非公平锁,不可重入锁。
ReentrantLock有公平和非公平锁两种实现,默认为非公平。
参见博客:
https://blog.csdn.net/u012403290/article/details/64910926
https://blog.csdn.net/zxd8080666/article/details/83214089

读写锁ReenntrantReadWriteLock
特性:写写互斥,读写互斥,读读共享
锁降级:写线程获取写入锁后可以获取读取锁,然后释放写入锁,这样就从写入锁变成读取锁,从而实现锁降级的特性 。
锁降级实例:

public class TestDegree {
    private int i = 0;

    private ReadWriteLock readWriteLock =  new ReentrantReadWriteLock();
    Lock readLock = readWriteLock.readLock();
    Lock writeLock = readWriteLock.writeLock();

    public void doSomrthing(){
        writeLock.lock();
        try{
            i++;
            readLock.lock();
        }finally {
            System.out.println("写释放了");
            writeLock.unlock();
        }
        try{//模拟复杂操作
            Thread.sleep(2000L);
        }catch (InterruptedException e){
            e.printStackTrace();
        }

        try{
            if(i == 1)
                System.out.println("i的值是>>>>>>>1");
            else
                System.out.println("i的值是" + i);
        }finally {
            System.out.println("读释放了");
            readLock.unlock();

        }
    }

    public static void main(String[] args) {
        TestDegree testDegree = new TestDegree();
        for (int i =0;i<4; i++){
            new Thread(()->{
                testDegree.doSomrthing();
            }).start();
        }
    }

}

结果:
在这里插入图片描述
获得写锁执行i++操作之后先不释放写锁,通过降级为读锁保证了数据在读之前没有被其他线程修改,读到的的自己刚修改的值。

Java8对读写锁的改进:StampedLock

该类是一个读写锁的改进,它的思想是读写锁中读不仅不阻塞读,同时也不应该阻塞写。

读不阻塞写:

在读的时候如果发生了写,则应当重读而不是在读的时候直接阻塞写!因为在读线程非常多而写线程比较少的情况下,写线程可能发生饥饿现象,也就是因为大量的读线程存在并且读线程都阻塞写线程,因此写线程可能几乎很少被调度成功!

当读执行的时候另一个线程执行了写,则读线程发现数据不一致则执行重读即可。所以读写都存在的情况下,使用StampedLock就可以实现一种无障碍操作,即读写之间不会阻塞对方,但是写和写之间还是阻塞的!

public  class  Point {

           //一个点的x,y坐标

           private   double   x,y;

           /**Stamped类似一个时间戳的作用,每次写的时候对其+1来改变被操作对象的Stamped值

            * 这样其它线程读的时候发现目标对象的Stamped改变,则执行重读*/

           private final   StampedLock  stampedLock   =  new    StampedLock();

           // an exclusively locked method

           void move(doubledeltaX,doubledeltaY) {

                   /**stampedLock调用writeLock和unlockWrite时候都会导致stampedLock的stamp值的变化

                  * 即每次+1,直到加到最大值,然后从0重新开始 */

                  longstamp =stampedLock.writeLock(); //写锁

                  try {

                         x +=deltaX;

                         y +=deltaY;

                  } finally {

                         stampedLock.unlockWrite(stamp);//释放写锁

                  }

           }

         double distanceFromOrigin() {    // A read-only method

                 /**tryOptimisticRead是一个乐观的读,使用这种锁的读不阻塞写

                 * 每次读的时候得到一个当前的stamp值(类似时间戳的作用)*/

                longstamp =stampedLock.tryOptimisticRead();

                //这里就是读操作,读取x和y,因为读取x时,y可能被写了新的值,所以下面需要判断

                double    currentX =x,   currentY =y;

                /**如果读取的时候发生了写,则stampedLock的stamp属性值会变化,此时需要重读,

                * 再重读的时候需要加读锁(并且重读时使用的应当是悲观的读锁,即阻塞写的读锁)

                 * 当然重读的时候还可以使用tryOptimisticRead,此时需要结合循环了,即类似CAS方式

                 * 读锁又重新返回一个stampe值*/

                if (!stampedLock.validate(stamp)) {

                        stamp =stampedLock.readLock(); //读锁

                        try {

                              currentX =x;

                              currentY =y;

                        }finally{

                              stampedLock.unlockRead(stamp);//释放读锁

                       }

                }

               //读锁验证成功后才执行计算,即读的时候没有发生写

               return Math.sqrt(currentX *currentX + currentY *currentY);

          }

}

实现思想:
在StampedLock中使用了CLH自旋锁,如果发生了读失败,不立刻把读线程挂起,锁当中维护了一个等待线程队列。

所有申请锁但是没有成功的线程都会记录到这个队列中,每一个节点(一个节点表示一个线程)保存一个标记位(locked),

用于判断当前线程是否已经释放锁。当一个未标记到队列中的线程试图获得锁时,会取得当前等待队列尾部的节点作为其前序节点,

并使用类似如下代码(一个空的死循环)判断前序节点是否已经成功的释放了锁:

    while(pred.locked){  }   

    解释:pred表示当前试图获取锁的线程的前序节点,如果前序节点没有释放锁,
    则当前线程就执行该空循环并不断判断前序节点的锁释放,

即类似一个自旋锁的效果,避免被系统挂起。当循环一定次数后,前序节点还没有释放锁,则当前线程就被挂起而不再自旋,

因为空的死循环执行太多次比挂起更消耗资源。
有关StampedLock引自:https://blog.csdn.net/sunfeizhi/article/details/52135136

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值