Java多线程笔记

进程的三个特

独立性:进程是系统中独立的实体,他可以拥有自己的资源,每一个进程都有自己的独立地址空间
动态性:程序是一个静态的指令集合,进程是系统中正在活动的指令集合,进程拥有自己的生命周期。
并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会相互影响。

进程和线程区别

进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体。进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时,它才能成为一个活动的实体,我们称其为进程。
一个进程中可以包含若干个线程,它们可以共享利用进程所拥有的资源。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位
线程是独立运行的,它并不知道进程中是否有其他线程的存在,线程的执行是抢占式的,当前运行的线程在任何时刻都可能被挂起以便另一个进程可以运行。
线程共享的环境包括:进程代码段、进程的公有数据等,利用这些共享数据进程可以很容易实现互相之间的通信。

线程与进程的区别归纳:
地址空间和其它资源:进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
调度和切换:线程上下文切换比进程上下文切换要快得多。

Java 线程创建

方法一:通过继承Thread类创建线程类
1、定义Thread子类,重写该类的run()方法,该run()方法体代表线程需要完成的任务
2、创建Thread子类的实例
3、调用线程对象的start()方法启动线程。

方法:

Thread.currentThread() //返回当前正在执行的线程对象
getName() // Thread类实例的方法,该方法返回调用该方法的线程的名字

注意:使用继承Thread类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量。

方法二:实现Runnable接口创建线程类
1、定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的执行体。
2、创建Runable()实现类的实例,并且以此实例为Thread的target来创建Thread对象
3、调用线程对像的start()方法来启动多线程。

注意:采用Runnable接口的方式创建的多线程可以共享线程类的实例变量。

方法三:使用Callable和Future创建线程
从java5开始,java提供了Callable接口,这个接口可以是Runnable接口的增强版,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能更强大。

call()方法特点:
1.call()方法可以有返回值
2.call()方法可以声明抛出异常。

Future接口
java5提供了Future接口来代表Callable接口里的call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该实现类实现了Future接口,并实现了Runnable接口,所以这样可以作为Thread的target。

在Future接口里定义了一下公共方法来控制它关联的Callable任务。

  1. boolean cancel( boolean mayInterruptRunning):试图取消该Future里关联的Callable任务。
  2. V get() :返回Callable任务里call()方法的返回值。调用该方法会导致程序阻塞,必须等到子线程结束后才会得到返回值。
  3. V get( long timeout, TimeUnit unit ):返回Callable任务里call方法的返回值。该方法然程序最多阻塞timeout和unit指定的时间,如果时间到了Callable没返回值,抛TimeoutException异常。
    4.boolean isCancelled():如果Callable任务正常完成前被取消,返回true。
    5.boolean isDone():如果Callable任务已完成,则返回true。

创建和启动线程的步骤
1.创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法没有返回值,再创建Callable实现类的实例。
2.使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
3.使用FutureTask作为Thread对象的target创建并启动新线程
4.调用FutureTask对象的get方法来获得子线程执行结束后的返回值。

创建线程的三种方式对比:
采用实现Runable,Callable接口的方式创建多线程的优缺点
1. 线程仅仅是实现了Runable或Callable接口,还可以继承其他类
2. 在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同的线程来处理同一份资源的情况,从而可以将CPU,代码和数据分开,形成清晰的模型
劣势:
如果想要访问当前线程必须使用Thread.currentThread()方法。

采用继承Thread方式创建多线程的优缺点
优势:线程继承了Thread,不能继承其他类了
劣势:编写简单,如果要访问当前线程,直接使用this即可

线程的生命周期

new、Runnable、Running、Blocked、Dead五种状态。

1、新建和就绪
当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,仅仅有Java虚拟机分配内存,并且初始化其成员变量的值。
当线程对象调用start()方法之后,该线程就处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于之歌状态中的线程并没有开始运行,仅仅表示我可以运行了。

注意:启动线程使用的是start()方法而不是run()方法,如果调用run()方法就会被立即执行,而且在run()方法返回之前其他线程无法并行执行,即直接调用run方法的话,系统会把线程对象当做一个普通的对象进行处理,run()也仅仅是一个方法,而不是线程执行体。

  1. 列表内容

运行和阻塞状态

当发生如下情况,线程会进入阻塞状态

  1. 线程调用sleep()方法主动放弃所占用的处理器资源
  2. 线程调用了一个阻塞式IO方法,在方法返回之前,该线程被阻塞
  3. 线程试图获得一个同步监视器,但该监视器被其他线程所持有
  4. 线程在等待某个通知
  5. 程序调用了线程的suspend()方法将该线程挂起。但这个方法容易造成死锁。

    发生以下情况,解锁让线程重新进入就绪状态:

  6. 调用sleep()方法的线程经过了指定的时间。

  7. 线程调用的阻塞式IO方法已经返回
  8. 成功获得同步监视器
  9. 处于挂起的线程被调用了resume()方法

    线程状态图(源于疯狂Java第三版):
    这里写图片描述

线程死亡:
线程死亡的三种情况
1、run(),call()方法结束
2、线程抛出了一个未捕获的异常Exception或Error。
3、直接调用线程stop()方法结束线程。

线程控制

1、join线程
Thread提供了一个线程等待另一个线程完成的方法——join()
当某个程序执行流调用了其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完成为止。

2、后台线程
后台线程的任务是为其他线程提供服务,在后台运行。
特征:如果所有的前台线程都是死亡了,后台线程自动死亡。

3、线程睡眠:sleep
Thread类静态方法sleep()可以让正在执行的线程暂停一段时间。
当线程调用sleep()方法进入阻塞状态后,在其睡眠的时间内,该线程不可获得执行机会。

4、线程让步:yied
Thread类的一个静态方法yield()可以让当前正在执行的线程暂停,但他不会阻塞线程,而是重新回到就绪状态。
实际上,当某个线程调用了yield之后只有优先级与当前进程相同后者优先级更高的并处于就绪状态的进程才会获得机会。

sleep和yield的区别:
1、sleep方法暂停当前线程后,会给其他线程执行,不理会优先级,但yield方法只会给优先级相同,或更高的线程执行。
2、sleep会把线程阻塞,结束后进入就绪状态,当yield不会阻塞线程,而是直接进入就绪状态,所以执行yield后的线程完全可能就立即进入运行状态,获得处理器资源
3、sleep()方法声明了抛出了InterruptedException异常,所以调用sleep()方法时要么捕捉该异常,要么显示声明抛出该异常;而yield方法则没有声明抛出任何异常

线程同步(重点)

1、同步代码块

synchronized(obj){
..........
}

上面代码中的obj是同步监视器,线程开始执行同步代码块之前,必须先获得对同步监视器的锁定
注意:任何一时刻只有一个线程可以获得对同步监视器的锁定,并且当同步代码块执行完之后,该线程会释放对同步监视器的锁定。即时Java程序允许使用任何对象作为同步监视器,但由于同步监视的目的:阻止两个线程对同一个共享资源进行并发访问,因此通常推荐使用肯能被并发访问的共享资源充当同步监视器。

2、同步方法
与同步代码块对应,Java的多线程安全支持还提供了同步方法,同步方法就是使用synchronized关键字来修饰某个方法,则方法称为同步方法。对于synchronized修饰的实例方法(非static方法)而言,无需显示指定同步监视器,同步方法的同步监视器是this,也就是调用该方法的对象。

线程安全的类具有的特点:
1.该类的对象可以被很多个线程安全地访问
2.每个线程调用该对象的任意方法之后都可以得到正确的结果
3.每个线程调用该对象的任意方法之后,该对象依然保持合理状态

注意:可变类的线程安全是以降低程序的运行效率为代价,减少负面影响策略:
1、不要对线程安全类的所有方法都进行同步,只对那些会改变的竞争支援的方法进行同步
2、如果可变类有两种运行环境:单线程和多线程环境,则应该提供两个不同版本的可变类来在不同的环境使用,如StringBuilder(单)和StringBuffer(多)

释放同步锁的情况:

  1. 当前线程的同步方法,同步代码块执行结束
  2. 当前线程的同步方法,同步代码块中遇到了break,return终止了执行
  3. 当前线程的同步方法,同步代码块遇到未处理的Error或Exception,导致当前线程的同步方法,同步代码块因异常结束
  4. 当前线程的同步方法,同步代码块执行时,执行了同步对象监视器的wait()方法,则当前线程暂停,并释放同步监视器。

不释放同步锁的情况:

  1. 线程执行同步代码块或同步方法时,程序调用了Thread.sheep(),Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器。
  2. 线程执行同步代码时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放同步监视器。

同步锁

Lock同步锁——通过显示定义同步锁对象来实现同步
Lock提供比synchronized方法和synchronized代码块更加广泛的锁的操作,lock允许实现更加灵活的结构,可以具有很差别很大的属性,并且支持多个相关的Condition对象。Lock控制多个线程对共享资源的访问,一般是独占式的访问,每一次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应该先获得Lock对象。

注意:使用Lock与使用同步方法有点相似,只是使用Lock时显式使用Lock对象作为同步锁,而使用同步方法时系统隐式使用当前对象作为同步监视器,同样符合“加锁”-“修改”-“释放锁”的操作模式,而且使用Lock对象时每一个Lock对象对应于当前使用锁的类的一个对象,一样可以保证对于同一对象,同一时刻只能有一个线程能进入临界区。

死锁

当两个线程互相等待对方释放同步监视器时就会发生死锁,Java虚拟机没有监测,也没有采取措施来处理死锁情况,所以多线程编程时应该采取措施避免死锁的出现。一旦死锁出现,不会发生任何异常,也不会给出任何提示,只有所有线程处于阻塞状
态。在有很多同步监视器时,死锁很容易发生。

线程通信(Java提供保证线程相互协调运行的机制)

1、传统的线程通信
借助Object提供的wait()、notify()和notifyAll()方法。这三个方法不是Thread类的方法,但这三个方法必须由同步监控器对象来调用
情况一:
对于使用synchronized修饰的方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用这三个方法。
情况二:
对于使用synchronized修饰的同步代码块,同步监视器是synchronized后括号的对象,所以必须使用该对象调用这三个方法。

使用Condition控制线程通信

如果程序不使用synchronized关键字来保证线程同步,而是直接使用Lock对象来保证同步,则系统中就不存在隐式的同步监视器。也就不能使用wait()、notify()、notifyAll()方法进行线程通讯了。Java提供Condition类来保持协调,使用Condition可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,Condition对象也可以唤醒其他处于等待的线程。

Condition实例被绑定在一个lock对象上。要获得Condition实例,调用Lock对象的newCondition()方法即可。
Condition方法:

await()
signal()
signalAll()

使用阻塞队列(BlockingQueue)控制线程通讯

BlockingQueue特征:
当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞,当消费者线程试图取出元素时,如果队列为空,则该线程被阻塞。

方法:

put(E e)
take(E e)

线程池

线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象或者Callable对象传给线程池,线程池就会启动一个线程来执行它们的run()方法或者call()方法,当run()方法或者call()方法结束之后,线程不会死亡,而是再次返回线程池中形成空闲状态,等待下一个Runnable对象或者Callable对象。并且线程池可以有效地控制线程数量。

Exectuors线程池工厂类创建线程池的静态方法:

ExecutorService newCachedThreadPool(int nThreads)
ExecutorService newFixedThreadPool(int nThreads)
ExecutorService newSingleThreadExecutor()
//java8新加的方法
ExecutorService newWorkStealingPool(int parallelism)
ExecutorService newWorkStealingPool()

ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
ScheduledExecutorService newSingleThreadScheduledExecutor(int corePoolSize)

//ExecutorService代表尽快执行线程的线程池
//方法:
Future<?> submint(Runnable task)
Future<?> submint(Runnable task,T result)
Future<?> submint(Callable<T> task)

//ScheduledExecutorService代表在指定延迟或周期性地执行线程任务的线程池
//方法:
ScheduledFuture<V> schedule(Callable<V> callable,long delay,TimeUnit unit)
ScheduledFuture<V> schedule(Runnable command,long delay,TimeUnit unit)
ScheduledFuture<V> scheduleAtFixedRate(Runnable command,long initalDelay,long period,TimeUnit unit)
ScheduledFuture<V> scheduleWithFixedRate(Runnable command,long initalDelay,long period,TimeUnit unit)

使用线程池执行步骤:
1、调用Executor类的静态工厂创建一个ExecutorService对象,该对代表一个线程池
2、创建Runnable实现类或Callable实现类的实例,作为线程执行任务
3、调用ExecutorService对象的submit()方法来提交Runnable实例或Callable实例
4、当不想提交任何任务时,调用ExecutorService对象的shutdown()方法来关闭线程池

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值