thinking in java ---21并发5

本文详细探讨了Runnable接口、线程池的使用(如CachedThreadPool、FixedThreadPool等)、线程优先级与守护线程、异常处理、线程间协作(wait/notify机制)、自定义线程工厂和ThreadLocal。讲解了为何推荐使用ExecutorService而非直接new Thread,以及如何利用线程池管理和控制线程行为。

1.当从Runnable导出一个类时,它必须具有run()方法,但是这个方法并无特殊之处 -- 它不会产生任何内在的线程能力,要实现线程能力,必须显示的将一个任务附着到线程上。

2.使用Executor替代显示的Thread对象,在任何线程池中,现有线程在可能的情况下,都会被自动复用

3.线程-Thread 任务-Runnable

4.线程池可以自定义线程工厂,这个很重要,后面比如批量设置thread.setDeamo(true),或者设置捕获Exception,都需要,记住线程池就是定义一个线程工厂,然后excute或者submit Runnable.可以使用默认工厂或者自己实现。下面是自定义的

Excutors.newCacheThreadPool(new ThreadFatory(){
    @Override
    public Thread newThread(Runnable r){
        Thread t = new Thread(r);
        return t;
    }
});

cachedThreadPool(缓存,线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程)

FixedThreadPool(创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待)

除了ScheduledThreadPool和返回参数的线程池,其他添加都是executorService.execute(new Runnable());

ScheduledThreadPool(创建一个定长线程池,支持定时及周期性任务执行)

ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
scheduledThreadPool.schedule(new Runnable() {
  
@Override
public void run() {
System.out.println("delay 3 seconds");
}
}, 3, TimeUnit.SECONDS);

newSingleThreadExecutor(创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行)其实就是数量为1的fixedThreadPool,适用于比如文件IO操作,避免多个线程操作同一资源

5.返回信息:实现Callable接口,List<Future<String>>.add(pool.submit(new Callable(){});

这种匿名内部内传参要么用类全局变量,要么局部变量为final,或者.accept 写个 public 

 public Callable<Map<String,Object>> accept(
                                        Map<String, Object> mapinfo)
                                {
                                    this.mapinfo = mapinfo;
                                    return this;
                                }

获取future的返回值可以用future.isDone()来判断是否执行完,然后get(),或者直接get()会一直阻塞直到执行完成

线程不会传递Exception到主线程,需要本地处理异常

线程的sleep及线程的分发,每个系统不一样,所以不要完全相信在你本地分发的策略

Thread.currentThread()返回当前线程引用

6.Thread.currentThread().setPriority(Thread.MAX_PRORITY OR Thread.NORM_PRIORITY OR Thread.MIN_PRIORITY)

一般不使用,Thread.yield()暗示可以让别的线程使用CPU了,仅仅是暗示,不代表会被采纳,其实不能依赖yield

设置线程优先级setPriority 只能在run方法里,如果在别的地方设置比如构造函数,将do no good(这是什么意思,不好的意思还是不能设置的意思,百度了一下应该是无济于事的意思),以为此事Executor在那个时间点还没有开始任务

7.后台程序:只要主干程序结束后台程序就结束,主干程序的非后台线程结束主干程序就结束。

Thread().setDaemon(true)设置后台程序

public class RunMore extends Thread implements Runnable{

	@Override
	public void run() {
		while(true) {
			try {
				Thread.sleep(100);
				System.out.println(Thread.currentThread());
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		
	}
	
	public static void main(String args[]) {
		for(int i =0;i<=10; i++) {
			Thread daemo = new Thread(new RunMore());
			daemo.setDaemon(true);
			daemo.start();
		}
		System.out.println("all thread start");
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

Thread.sleep()这个时间设置的越小  主干程序存活的时间越小,后台线程打印的数量越小。

setDeamon这个方法的意思是设置守护线程,附属于主线程,主线程结束则附属线程结束

ExcutorService exec = Excutors.newCachedThreadPool(new ThreadFactory);

可以重写ThreadFactory的newThread方法,让thread都以deamo状态来运行,Excutors可以重新属性(daemon,priority,name)

(衍生一下,ThreadFactory还可以加统一异常处理,思路:定义一个UncaughtExceptionHandler,然后ThreadFactory.setUncaughtExceptionHandler(new yourHandler),然后Excutors.newCachedThreadPool(your ThreadFactory),然后excutor.execute(new Runable());

deamo的线程衍生出来的线程都是deamo

public class DaemonThreadFactory implements ThreadFactory {
    public Thread newThread(Runnable r) {
    Thread t = new Thread(r);
    t.setDaemon(true);
    return t;
    }
}

public class DaemonFromFactory implements Runnable {
    public void run() {
        try {
            while(true) {
                TimeUnit.MILLISECONDS.sleep(100);
                print(Thread.currentThread() + " " + this);
              }
            } catch(InterruptedException e) {
        print("Interrupted");
    }
}
public static void main(String[] args) throws Exception {
    ExecutorService exec = Executors.newCachedThreadPool(
        new DaemonThreadFactory());
    for(int i = 0; i < 10; i++)
        exec.execute(new DaemonFromFactory());
        print("All daemons started");
    TimeUnit.MILLISECONDS.sleep(500); // Run for a while
    }
}

deamo线程如果有try catch finally 那么finally这段代码不会执行(为啥呢,这个英文有点翻译不来,大概意思是守护进程可能随时被主程序终止,finally代码是不可靠执行的,所以干脆就不给你这个思想,让你别realy on finally)

8.为啥建议不直接new Thread(new Runable)呢  而是建议用Thread工厂,比如Excutors.CacheExcutorPool这种,因为你直接Thread,书上有种情况是在构造函数里面直接start(),这种代码第一不好看,第二是可能某些参数还没有初始化导致出错,书上这样写的,其实也不会这么傻吧。

9.加入一个线程t.join();这样子的:当一个主线程调用另外一个线程t,t.join(),然后主线程开始暂停等待t执行完再执行(t.isAlive()为false); 加入线程join(),必须要捕获编译异常InterruptException(),当主线程.interrupt() 就会被捕获,但是在被捕获后isInterrupt()这个方法会被清除标志,本来应该是true(已经被终止),但是捕获后再打印出来依旧是false;

.interrupt()这个方法只是标识一个中断标识,并不会立即中断线程,比如sleep会检测这个中断标志,如果是true,则抛出InterruptException();

来来来,看了几篇interrupt的文章,总算了解到皮毛了,是这样一个意思:线程理应该自己中断进程,不应该由其他进程帮忙中断,所以已经放弃了Thread.stop()等一些方法。然而这个方法只是一个标识更改,并不会导致程序结束,除了在阻塞状态(sleep waite join 同步锁等状态)在收到interrupt这个状态更改后会立即抛出一个interruptException,在正常的执行的没有阻塞状态不会抛出,会正常继续执行。为什么呢?因为在阻塞的时候你没法知道已经给了一个阻塞标识,所以需要抛出。正常情况是在线程里需要一直(this.isInterrupted())进行检测是否置于终止状态,然后进行处理,比如退出或者做其他的。

isInterrupted()是否中断,不会改变终止状态

interrupted是否中断,但会更改终止状态,也就是如果是终止状态则更改为非终止

while (!Thread.interrupted()) {   //一般这样做
        // do more work.
    }

join也可以添加时间参数,如果这个时间添加的t没有处理完,直接返回进行主线程。

10.线程如何处理异常:我们用Excutors管理线程,Thread.UncaughtExceptionHandler可以绑定线程,当线程发生异常,会自动调用Thread.UncaughtException.uncaughtException().所以我们要做的就是新建一个ExcutorService并且指定自定义ThreadFactory,里面绑定UncaughtExceptionHandler给每新建的线程.

class MyUncaughtExceptionHandler implements
    Thread.UncaughtExceptionHandler {
        public void uncaughtException(Thread t, Throwable e) {
        System.out.println("caught " + e);
    }
}
class HandlerThreadFactory implements ThreadFactory {
    public Thread newThread(Runnable r) {
        System.out.println(this + " creating new Thread");
        Thread t = new Thread(r);
        System.out.println("created " + t);
        t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
        System.out.println(
        "eh = " + t.getUncaughtExceptionHandler());
        return t;
    }
}
//创建ExcutorService
ExecutorService exec = Executors.newCachedThreadPool(
new HandlerThreadFactory());

如果不需要每个线程对应不同的Exception,而是设置一个全局的捕获异常操作,可以这样做

Thread.setDefaultUncaughtExceptionHandler(
new MyUncaughtExceptionHandler());

当没有单独设置异常处理器,才会去调用这个Default默认处理器,如果有设置过,那就不会去调

11.解决资源争夺,在语句前加上一个锁协议,成为互斥锁,mutex。关键字synchronized。比较重要的一点,如果使用多线程,需要让字段表示为private,否则synchronized不会防止其他任务访问字段。

synchronized void f() { /* ... */ } 
synchronized void g() { /* ... */ } 

For the preceding methods, if f( ) is called for an object by one task, a different task cannot call f( ) or g( ) for the same object until f( ) is completed and releases the lock. Thus, there is a single lock that is shared by all the synchronized methods of a particular object,

一个任务多次获取同一个对象的锁,因为肯定只有一个task能获得锁,在第一次获取的时候jvm会加锁计数加1,每获取一次加一(这种情况是这个对象获取了这个锁,可以在方法里继续获取锁,只能相同任务才能多次获取锁),离开一个synchronized减一,直到变成0就可以给其他task用。

看了下博客,有一个点要搞懂:如果一个对象有多个synchronized方法,只要一个线 程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法,也就是说一个对象只有一把锁,是一个对象一个实例一把锁,一个对象不同实例相互不影响。比如synchronized(this)[只作用于这个对象]和synchronized(XXX.class)[作用于这个类下所有实例对象]是不一样的。然后我想了一下,比如我们在一个类里面有两个方法都要同步method a()和 method b(),但是只要有一个实例调用了a就不能有其他实例调用b,但是我想实现的是,不能有任务同时调用a或同时调用b,交叉调用没关系,那就每个方法定义一个锁对象,比如a(){synchronized(new 一个实例)) b(){synchronized(new 一个其他实例))那么是两个方法两个锁,就不会互相影响

class A{
    private A a1 = new A();
    private A a2 = new A();

    public void K(){
        synchronized(a1){
            ....
        }
    }

    public void M(){
        synchronized(a2){
            ...
        }
    }

synchronized static method可以在类的范围内对对象进行锁,不是对象的实例

12.获取锁有两种方式,一种是利用synchronized关键字,一种是显示获取锁

public class MutexEvenGenerator extends IntGenerator {
    private int currentEvenValue = 0;
    private Lock lock = new ReentrantLock();
    public int next() {
        lock.lock();
        try {
            ++currentEvenValue;
            Thread.yield(); // Cause failure faster
            ++currentEvenValue;
            return currentEvenValue;
        } finally {
            lock.unlock();
        }
}

那什么时候用关键字什么时候用显示锁呢,有一个情境,比如你要先看看这段代码有没有被锁,等待一秒钟还没解锁就要返回到interface,那就可以用显示的,先lock.tryLock(),如果false就代表被锁了,就可以返回说有线程在用。

try {
captured = lock.tryLock(2, TimeUnit.SECONDS);
} catch(InterruptedException e) {
throw new RuntimeException(e);
}

但是用显示的lock要注意必须try catch finnali(释放锁),并且return语句应该放在try里面,因为怕锁先释放后,暴露了data给第二个任务

13.原子性和易变性(volatility)

只要是原子性的操作就不用担心同步问题,只要开始就不会context switch,比如除了long和double的基础类型的操作都是原子性,但是long和double是64位的类型,所以在32位操作系统上是分两步进行赋值,因此在32位上不是原子性的,加上volatile就可以保证原子性。说了很多,就是说我们这种人是达不到只写原子操作的代码的,让我们不要想了,该用syncronized还是要用,不要觉得自己牛逼。

讲了一个多核处理器可视性visibility更重要比原子性,比如一个task改变了某个东西,存在缓存里,其他的task对于这个改变就是不可见的,但是syncronized同步系统是强制性的可见

volatile也保证了在应用中的可视性(visibility),如果一个字段用volatile修饰,标识只要写入这个字段,所有的读写都可以观察到他的变化,就算用了local caches。

好了,看了一会的volatile和syncronized后,总结一下:volatile能保证可见性,不能保证原子性,sysnconized保证原子性,从而也保证了可见性,synconized肯定消耗资源更多。为什么volatile不能保证原子性,比如一个状况:对于一个i进行赋值,有十个线程进行赋值,在一个线程取出i放入工作内存和寄存器后,其他线程进行更改i,虽然volatile可以保证可见性,但是寄存器里的值还是以前的不会更新,只有工作内存的值无效重新获取,所以导致不是原子性的。再看看一个解释的

既然i是被volatile修饰的变量,那么对于i的操作应该是线程之间是可见的啊,就算A.,B两个线程都同时读到i的值是5,但是如果A线程执行完i的操作以后应该会把B线程读到的i的值置为无效并强制B重新读入i的新值也就是6然后才会进行自增操作才对啊。

后来参照其他博客终于想通了:

1、线程读取i

2、temp = i + 1

3、i = temp
当 i=5 的时候A,B两个线程同时读入了 i 的值, 然后A线程执行了 temp = i + 1的操作, 要注意,此时的 i 的值还没有变化,然后B线程也执行了 temp = i + 1的操作,注意,此时A,B两个线程保存的 i 的值都是5,temp 的值都是6, 然后A线程执行了 i = temp (6)的操作,此时i的值会立即刷新到主存并通知其他线程保存的 i 值失效, 此时B线程需要重新读取 i 的值那么此时B线程保存的 i 就是6,同时B线程保存的 temp 还仍然是6, 然后B线程执行 i=temp (6),所以导致了计算结果比预期少了1。如果不加sychronized,值永远小于预期

也就是temp在寄存器里面,还有一层高速缓存。自增操作不是线程安全的

还讲了些原子性的东西,但我在jdk7出现的结果和书上不一样,就不深究了,总结下就是说尽量不要用volilate,尽量用锁(隐实锁synchronized和显示锁lock)

可以同步方法也可以同步代码块 用synchronized(object){}来进行标记,同步代码块能提高访问对象的性能,不像整个方法的同步,不会一直锁住线程安全的对象。一般同步临界用的object就是this,比如synchronized(this)

再来:volilate能保证可见性和有序性:有序性表示可能某一步比如新建对象,new instance();

当不用volilate时,处理器可能会优化而进行指令重排,正常应该是1.分配空间 2.实例化对象放入空间 3. 引用指向空间。如果重排  则2 3 可能调换位置,导致另外一个线程读取到instance已经非空也就是实例化了,再进行其他步骤导致出错。

所以volilate不适用于依赖前一个值的时候,比如i++,尽管加了volilate,只是主内存能看见,但是i++依然分成了很多个步骤,只保证了可见性,没保证原子性。

https://www.cnblogs.com/dolphin0520/p/3920373.html

ThreadLocal:

ThreadLocal<HttpRequest> request = new ThreadLocal<HttpRequest>;
request.get();
request.set(HttpRequest);

简而言之,就是比如有一个变量需要在很多地方用,一般我们会怎么做,我们会把这个变量设置为static,但是有个问题,这样不是线程安全的,所以使用ThreadLocal来保证每个线程都有自己的副本,不会相互影响。反正就是你用static那肯定多个线程获取的对象就会改变,因此这样做。

ThreadLocal不是为了解决资源共享和并发问题,是为了每个线程能保存自己的变量

最典型的是管理数据库的Connection:当时在学JDBC的时候,为了方便操作写了一个简单数据库连接池,需要数据库连接池的理由也很简单,频繁创建和关闭Connection是一件非常耗费资源的操作,因此需要创建数据库连接池~

那么,数据库连接池的连接怎么管理呢??我们交由ThreadLocal来进行管理。为什么交给它来管理呢??ThreadLocal能够实现当前线程的操作都是用同一个Connection,保证了事务!

Thread有四种状态

:new、runnable、blocked、dead

变成blocked的方式:sleep(milliseconds)、wait()[这个方法如果不调用notify()或者notifyAll()不会回到runnable状态]、这个线程正在等待IO、线程去调用synchronized方法或者代码(已经被别的线程获取了锁)。取消了suspend()和resume()因为会造成死锁,取消了stop()因为可能造成运行的不稳定,突然停止某个程序,使之无法释放锁,并且能被修改。

Thread.interrupt(),抛出IterruptedException以及轮训中断Thread.interrupted()会重置中断标志

现在一般都不直接操作Thread,而是操作Excutor线程池,也就是Excutor.shutdownNow(),将会发送给每个线程Thread.interrupt();那如何由Excutor停止某一个线程呢,那就是不用execute而是使用submit获取一个Future泛型对象,future.cancle();

当处于block状态,调用interrupt(),会将终止状态设置为true,如果是sleep或者wait方法,则直接抛出interruptException,否则继续执行,只是状态改了,在哪里进行终止由执行代码决定。也就是说你能中断任何要求抛出InterruptedExcetion的调用,但不能中断比如等待获取锁的同步程序或者IO任务。如果要中断IO阻塞,可以从底层资源关闭,比如socket.close();\

锁阻塞应该如何停止,使用显示锁Lock,并调用lock.lockInterruptibly(),当线程被t.interrupt,则会抛出InterruptedException异常。

当被互斥锁阻塞

当线程t.interrupt,只有几个情况能被动中断线程:

1.sleep
2.wait
3.join
4.lock.lockInterruptibly

,也就是synchronized或者显示锁Lock,显示锁可以被中断,reentrantLock(),lock.lockInterruptibly();

 ReentrantLock获取锁定与三种方式:
    a)  lock(), 如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于休眠状态,直到获取锁

    b) tryLock(), 如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false;

    c)tryLock(long timeout,TimeUnit unit),   如果获取了锁定立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false;

    d) lockInterruptibly:如果获取了锁定立即返回,如果没有获取锁定,当前线程处于休眠状态,直到或者锁定,或者当前线程被别的线程中断

线程间的协作wait()和notifyALL()

wait必须在synchronized方法里面使用,否则会报错IllegalMonitorStateException,相当于说,wait表示:我已经做完了我的了,给其他线程做,如果不是给同步线程使用,那可以直接使用yield,所以这是线程协作的场景,也就是共同使用一个共享资源进行协作。需要同步.在调用wait()、notify()、notifyAll()之前,必须获取锁。

sleep()也和wait()差不多,

1.但是sleep不会被唤醒,wait()可以被notify()、notifyAll()唤醒

2.第二sleep不会释放锁,wait会释放锁

一样的:都可以通过构造时间,到时了自动恢复。

wait两种构造方式,wait()和wait(millions),和sleep一个道理 ,只不过会释放锁,等多少时间自动恢复,没有构造参数的只能通过notify()或者notifyAll()唤醒

wait()、notify()、notifyAll()都是基类object的一部分,不是thread得一部分,调用wait()、notify()、notifyAll()只能在同步控制方法或同步控制块里调用,因为需要先获取锁。但是sleep()就不需要,随便哪里都可以。

记住,wait()需要用while循环包裹,可能说如果只有你用notify,就其实不需要循环条件,就是怕其他你不知道的地方,比如继承了你的类进行Notify,你的条件不满足就进行处理,就会出问题

如何利用wait()、notify()、notifyAll(),这里讲了一个生产者消费者的场景,比如生产者要生产必须库存小于总库存,消费者要消费必须库存大于0.库存为锁对象,因为生产者和消费者都会操作库存。当库存为0,消费者需要等待并且把锁释放给其他线程(比如下一个消费者或者生产者),并且wait()外面必须由while循环包裹进行唤醒后的判断是否能进行操作或者继续等待。当生产者获得锁并且生产了一个,就立即notifyAll,如果是notify某一个线程可能造成死锁,详情可以看https://www.jianshu.com/p/25e243850bd2?appinstall=0

public class Something {
    private Buffer mBuf = new Buffer();

    public void produce() {
        synchronized (this) {
            while (mBuf.isFull()) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            mBuf.add();
            notifyAll();
        }
    }

    public void consume() {
        synchronized (this) {
            while (mBuf.isEmpty()) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            mBuf.remove();
            notifyAll();
        }
    }

notify造成死锁:比如出现一个情况,锁队列只有一个消费者a,等待队列有很多个生产者,消费者a消费后notify生产者b,这时a、b都在锁队列,a又抢到了锁然后进入等待队列,b生产后notify另外一个生产者c,因此b、c进行获取锁并进入等待区,所有线程都进入等待区导致死锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值