1、程序、进程、线程的理解。
程序:是为完成特定任务,用某种语言编写的一组指令的集合,即指一段静态的代码,静态对象。
进程:是程序的一次执行过程,或是正在运行的一个程序,是一个动态的过程,有它自身的产生,存在和消亡的
过程。-------生命周期
线程:进程可进一步细化为线程,是一个程序内部的一条执行路径
2、并行与并发
并行:多个CPU同时执行多个任务,比如:多个人同时做不同的事。形似多条平行线。
并发:一个CPU(采用时间片)同时执行多个任务,比如秒杀平台,多个人做同件事。形似多条交线,交点处视
为并发。
3、创建多线程的两种方式
方式一:继承Thread类
*(1)、创建继承于Thread类的子类
*(2)、重写Thread类的run()方法-->将此线程执行的操作声明在run()中
*(3)、创建Thread类的子类的对象
*(4)、通过此对象调用start():①.启用当前线程 ②调用当前线程的run()
说明:
问题一:启动一个线程,必须调用start(),不能调用run()的方式启动线程。
问题二:如果再启动一个线程,必须重新创建一个Thread子类的对象,调用此对象的start()
public class Test003 extends Thread{
@Override
public void run() {
System.out.println("111");
}
public static void main(String[] args) {
Test003 test003 = new Test003();
test003.start();
}
}
方式二:实现Runnable接口
*(1)、创建一个实现了Ruannable接口的类
*(2)、实现类中实现Runnable中的抽象方法:run()
*(3)、创建实现类的对象
*(4)、将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
*(5)、通过Thread类的对象调用start()
public class Test003 implements Runnable{
@Override
public void run() {
System.out.println("222");
}
public static void main(String[] args) {
Test003 test003 = new Test003();
Thread thread = new Thread(test003);
thread.start();
}
}
比较创建线程的两种方式:
开发中,优先选择实现Runable接口的方式
原因:
1:实现的方式没有类的单继承性的局限性
2:实现的方式更适合用来处理多个线程有共享数据的情况:继承的方式中共享数据通常需要添加static来保证唯
一性。
4、JDK5新增的线程创建的方式
方式三:实现Callable接口方式
与使用runnable方式相比,callable功能更强大些:
runnable重写的run方法不如callaalbe的call方法强大,call方法可以有返回值
方法可以抛出异常
支持泛型的返回值
需要借助FutureTask类,比如获取返回结果
步骤:
*(1)、创建一个实现Callable接口的实现类
*(2)、实现类中重写call()方法-->将此线程执行的操作声明在call()中
*(3)、创建该实现类的对象
*(4)、将该实现类作为参数传到FutureTask类的构造器中,创建FutureTask类的对象。
*(5)、将Futuretask类的对象作为参数传入Thread类的构造器中,创建Thread类的对象。
*(6)、通过Thread类的对象调用start()方法。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Test004 implements Callable{
private int sum=0;//
@Override
public Object call() throws Exception {
for(int i = 0;i<=100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName()+":"+i);
sum += i;
}
}
return sum;
}
public static void main(String[] args) {
Test004 test004 = new Test004();
FutureTask futureTask = new FutureTask(test004);
Thread thread = new Thread(futureTask);
thread.setName("线程1");
thread.start();
try {
Object sum = futureTask.get();//通过get()获取futureTask的返回值
System.out.println(Thread.currentThread().getName()+":"+sum);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
线程池-参考文档:
1、线程池管理工具 ThreadPoolExecutor 详解
2、聊聊并发(三)Java线程池的分析和使用
方式四:创建线程池的方式
背景:当线程使用量特别大的场景下,频繁的创建和销毁线程会导致运行速度的降低和内存的消耗。很大程度的
影响了程序的性能。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁,实现
重复利用。(类似生活中公共资源的使用)数据库连接池也是同样的道理。
好处:1、提高响应速度(减少线程创建的时间)
2、降低资源消耗(重复利用线程池中线程,不需要每次都创建)
3、便于线程管理
线程池的创建步骤:
1、创建实现runnable接口或callable接口的类的对象
2、创建ExcutorService线程池对象excutorService
3、调用excutorService的excute()方法<runnable方式>或submit()方法<callable方式>
4、关闭线程池shutdown()
*工厂类中的四种创建线程池的方法:
①Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
②Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程池
③Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
④Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执
行。
注意:
①.Excutors:工具类,线程池的工厂类,调用创建线程池的方法时(如newFixThreadPool(i)),其实是new了一
个ThreadPoolExcutor类,而ThreaPoolExcutor类继承了AbstractExecutorService抽象类,
AbstractExecutorService又实现了ExcutorService接口。
②.ExcutorService:是一个接口,该接口继承了Executor接口。
所以:线程池的主要实现方法都在ThreadPoolExcutor类中。
简例:
class NumberThread implements Runnable{
@Override
public void run() {
Thread.currentThread().setName("Number-Thread");
System.out.println(Thread.currentThread().getName()+":111");
}
}
class StringThread implements Callable{
@Override
public Object call() throws Exception {
Thread.currentThread().setName("String-Thread");
System.out.println(Thread.currentThread().getName()+"222");
return null;
}
}
public class Test005{
public static void main(String[] args) {
//利用Executors创建固定个数的线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
//创建线程
NumberThread numberThread = new NumberThread();
StringThread stringThread = new StringThread();
//执行线程,根据线程池大小
executorService.execute(numberThread);//execute()适用于实现runnable方式的线程
executorService.submit(stringThread);//submit()适用于实现callable方式的线程
//关闭线程池
executorService.shutdown();
}
}
5、Thread类中的常用方法
*(1)、start():启动当前线程:调用当前线程的run()
*(2)、run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
*(3)、Thread.currentThread():静态方法,返回当前代码的线程
*(4)、getName():获取当前线程的名称
*(5)、setName():设置当前线程的名称
*(6)、yield():释放当前cpu执行权,但不一定切换了线程:有可能该线程会再次抢到cpu执行权
*(7)、join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,知道线程b完全执行完以后,线程a
才结束阻塞状态
*(8)、stop():已过时,当执行此方法时,强制结束当前线程
*(9)、sleep(long millitime):让当前线程“睡眠”指定的millitime毫秒,在指定的millitime毫秒时间内,
当前线程是阻塞状态
*(10)、isAlive():判断当前线程是否存活
线程优先级涉及的方法:
===Java 线程优先级使用 1 ~ 10 的整数表示:===
最低优先级 1:Thread.MIN_PRIORITY
最高优先级 10:Thread.MAX_PRIORITY
普通优先级 5:Thread.NORM_PRIORITY
===如何获取当前线程的优先级:===
getPriority():获取线程优先级
setPriority():设置线程优先级
public static void main(String[] args) {
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
System.out.println(Thread.currentThread().getPriority());
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
System.out.println(Thread.currentThread().getPriority());
Thread.currentThread().setPriority(8);
System.out.println(Thread.currentThread().getPriority());
}
注意:线程优先级设置范围为1-10,否则抛出 java.lang.IllegalArgumentException 异常
public static void main(String[] args) {
Thread.currentThread().setPriority(0);
System.out.println(Thread.currentThread().getPriority());
}
总结(线程优先级):
1、优先级可以用从1到10的范围指定。10表示最高优先级,1表示最低优先级,5是普通优先级。
2、优先级最高的线程在执行时被给予优先。但是不能保证线程在启动时就进入运行状态。
3、与在线程池中等待运行机会的线程相比,当前正在运行的线程可能总是拥有更高的优先级。
4、由调度程序决定哪一个线程被执行。
5、t.setPriority()用来设定线程的优先级。
6、在线程开始方法被调用之前,线程的优先级应该被设定。
7、当线程的优先级没有指定时,所有线程都携带普通优先级。
8、高优先级的线程比低优先级的线程有更高的几率得到执行,实际上这和操作系统及虚拟机版本相关,有可能
即使设置了线程的优先级也不会产生任何作用。
6、线程的生命周期
详细说明:
*新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
*就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪
状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即
就会执行;
*运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状
态,此时才真正执行run()方法。
注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪
状态中;
*阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入
阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻
塞状态又可以分为三种:
1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
2.同步阻塞:线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
3.其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()
状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
*死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
Java多线程的就绪、运行和死亡状态:
就绪状态转换为运行状态:当此线程得到处理器资源;
运行状态转换为就绪状态:当此线程主动调用yield()方法或在运行过程中失去处理器资源。
运行状态转换为死亡状态:当此线程线程执行体执行完毕或发生了异常。
此处需要特别注意的是:当调用线程的yield()方法时,线程从运行状态转换为就绪状态,但接下来CPU调度就
绪状态中的哪个线程具有一定的随机性,因此,可能会出现A线程调用了yield()方法后,接下来CPU仍然调度了
A线程的情况。
7、线程的同步机制
1、同步机制的诞生:当多个线程对同一个共享数据进行操作时,线程没来得及更新该共享数据,从而导致另外
线程没得到最新的数据,从而产生线程安全问题。引入线程的同步机制即为了解决线程安全问题。
2、线程同步机制有哪些?(处理线程安全的方式有哪些)
线程的同步:在同步代码中,将多线程转变成单线程执行。
(1)、同步代码块:
使用同步监视器(锁)
Synchronized(同步监视器){
//需要被同步的代码
}
说明:
操作共享数据的代码(所有线程共享的数据的操作的代码)(视作卫生间区域(所有人共享的厕所)),
即为需要共享的代码(同步代码块,在同步代码块中,相当于是一个单线程,效率低)
共享数据:多个线程共同操作的数据,比如公共厕所就类比共享数据
同步监视器(俗称:锁):任何一个的对象都可以充当锁。(但是为了可读性一般设置英文成lock)当锁
住以后只能有一个线程能进去(要求:多个线程必须要共用同一把锁,比如火车上的厕所,同一个标志表
示有人)
Runable天生共享锁,而Thread中需要用static对象或者this关键字或者当前类(window.class)来充
当唯一锁
(2)、同步方法:
使用同步方法,对方法进行synchronized关键字修饰
将同步代码块提取出来成为一个方法,用synchronized关键字修饰此方法。
对于runnable接口实现多线程,只需要将同步方法用synchronized修饰
而对于继承自Thread方式,需要将同步方法用static和synchronized修饰,因为对象不唯一(锁不唯一)
总结:1.同步方法仍然涉及到同步监视器,只是不需要我们显示的声明。
2.非静态的同步方法,同步监视器是this
静态的同步方法,同步监视器是当前类本身。继承自Thread.class
(3)、lock锁(JDK1.5引入):
①、实例化lock锁:ReentrantLock lock = new ReentrantLock();
②、在需要的位置调用lock()方法;
③、同步结束的位置释放锁,调用unlock()方法;
public class Test006 implements Runnable{
private int ticket = 100;//定义一百张票
//实例化lock锁
ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
System.out.println("111");
//调用锁定方法lock
lock.lock();
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售出第" + ticket
+ "张票");
ticket--;
} else {
System.out.println("222");
}
//释放锁
lock.unlock();
}
}
class LockTest {
public static void main(String[] args){
Test006 w= new Test006();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
8、线程通信
(1)、通信方法:
wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
notify():一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程,就唤醒优先级高的线程。
notifyAll():一旦执行此方法,就会唤醒所有被wait()的线程。
(2)、使用前提:这三个方法均只能使用在同步代码块或者同步方法中。
说明:
①、wait()/ notify()/ notifayAll():此三个方法定义在Object类中的,因为这三个方法需要用到锁,而锁
是任意对象都能充当的,所以这三个方法定义在Object类中。
②、wait(在进入锁住的区域以后阻塞等待,释放锁让别的线程先进来操作)---- Obj.wait 进入Obj这个锁
住的区域的线程把锁交出来原地等待通知。
③、notify(由于有很多锁住的区域,所以需要将区域用锁来标识,也涉及到锁) ----- Obj.notify 新线程
进入Obj这个区域进行操作并唤醒wait的线程。
④、由于wait,notify,以及notifyAll都涉及到与锁相关的操作。所以wait,notify需要使用在有锁的地方,
也就是需要用synchronize关键字来标识的区域,即使用在同步代码块或者同步方法中,且为了保证wait和
notify的区域是同一个锁住的区域,需要用锁来标识,也就是锁要相同的对象来充当。
(3)、简例:
/**
* 线程通信的例子:使用两个线程打印1—100,线程1,线程2交替打印
*
* 当我们不采取线程之间的通信时,无法达到线程1,2交替打印(cpu的控制权,是自动分配的)
* 若想达到线程1,2交替打印,需要:
* 1.当线程1获取锁以后,进入代码块里将number++(数字打印并增加)操作完以后,为了保证下个锁为线程2
* 所有,需要将线程1阻塞(线程1你等等wait())。(输出1,number为2)
* 2.当线程2获取锁以后,此时线程1已经不能进入同步代码块中了,所以,为了让线程1继续抢占下一把锁,
* 需要让线程1的阻塞状态取消(通知线程1不用等了notify()及notifyAll()),即应该在进入同步代
* 码块时取消线程1的阻塞。
*
* */
class Number implements Runnable{
private int number = 1;//设置共享数据(线程之间对于共享数据的共享即为通信)
//对共享数据进行操作的代码块,需要线程安全
@Override
public synchronized void run() {
while(true){
//使得线程交替等待以及通知交替解等待
notify();//省略了this.notify()关键字
if(number<100){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+number);
number++;
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
}
}
}
public class CommunicationTest {
public static void main(String[] args){
//创建runnable对象
Number number = new Number();
//创建线程,并实现runnable接口
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
//给线程设置名字
t1.setName("线程1");
t2.setName("线程2");
//开启线程
t1.start();
t2.start();
}
}
(4)、sleep和wait的异同:
相同点:一旦执行方法以后,都会使得当前的进程进入阻塞状态
不同点:
1.两个方法声明的位置不同,Thread类中声明sleep(),Object类中声明wait()。
2.调用的要求不同,sleep()可以在任何需要的场景下调用,wait()必须使用在同步代码块或者同步方法中
3.关于是否释放同步监视器,如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放,wait()会
释放
4.sleep()阻塞相应时间后会自动唤醒,wait()需要调用notify()或notifyAll()才会唤醒。
9、线程死锁
1. java中导致死锁的原因
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放,而该资源又被其他线程锁定,从而
导致每一个线程都得等其它线程释放其锁定的资源,造成了所有线程都无法正常结束。这是从网上其他文档
看到的死锁产生的四个必要条件:
*1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
*2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
*3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
*4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成
了一个等待环路。
当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁
消失。下面用java代码来模拟一下死锁的产生。
2、简例:
(1)、模拟两个资源:
public class ThreadResource
{
public static Object resource1 = new Object();
public static Object resource2 = new Object();
}
(2)、模拟线程1占用资源1并申请获得资源2的锁:
public class Thread1 implements Runnable
{
@Override
public void run()
{
try
{
System.out.println("Thread1 is running");
synchronized (ThreadResource.resource1)
{
System.out.println("Thread1 lock resource1");
Thread.sleep(2000);//休眠2s等待线程2锁定资源2
synchronized (ThreadResource.resource2)
{
System.out.println("Thread1 lock resource2");
}
System.out.println("Thread1 release resource2");
}
System.out.println("Thread1 release resource1");
}
catch (Exception e)
{
System.out.println(e.getMessage());
}
System.out.println("Thread1 is stop");
}
}
(3)、模拟线程2占用资源2并申请获得资源1的锁:
public class Thread2 implements Runnable
{
@Override
public void run()
{
try
{
System.out.println("Thread2 is running");
synchronized (ThreadResource.resource2)
{
System.out.println("Thread2 lock resource2");
Thread.sleep(2000);//休眠2s等待线程1锁定资源1
synchronized (ThreadResource.resource1)
{
System.out.println("Thread2 lock resource1");
}
System.out.println("Thread2 release resource1");
}
System.out.println("Thread2 release resource2");
}
catch (Exception e)
{
System.out.println(e.getMessage());
}
System.out.println("Thread2 is stop");
}
}
(4)、同时运行俩个线程:
public class ThreadTest
{
public static void main(String[] args)
{
new Thread(new Thread1()).start();
new Thread(new Thread2()).start();
}
}
最后输出结果是:
Thread1 is running
Thread2 is running
Thread1 lock resource1
Thread2 lock resource2
并且程序一直无法结束。这就是由于线程1占用了资源1,此时线程2已经占用资源2,。这个时候线程1想要
使用资源2,线程2想要使用资源1,。两个线程都无法让步,导致程序死锁。
3、java避免死锁的解决意见
由上面的例子可以看出当线程在同步某个对象里,再去锁定另外一个对象的话,就和容易发生死锁的情况。
最好是线程每次只锁定一个对象并且在锁定该对象的过程中不再去锁定其他的对象,这样就不会导致死锁了。
比如将以上的线程改成下面这种写法就可以避免死锁:
public void run()
{
try
{
System.out.println("Thread1 is running");
synchronized (ThreadResource.resource1)
{
System.out.println("Thread1 lock resource1");
Thread.sleep(2000);//休眠2s等待线程2锁定资源2
}
System.out.println("Thread1 release resource1");
synchronized (ThreadResource.resource2)
{
System.out.println("Thread1 lock resource2");
}
System.out.println("Thread1 release resource2");
}
catch (Exception e)
{
System.out.println(e.getMessage());
}
System.out.println("Thread1 is stop");
}
但是有的时候业务需要同时去锁定两个对象,比如转账业务:A给B转账,需要同时锁定A、B两个账户。如果
A、B相互同时转账的话就会出现死锁的情况。这时可以定义一个规则:锁定账户先后的规则。根据账户的某
一个属性(比如id或者hasCode),判断锁定的先后。即每一次转账业务都是先锁定A再锁定B(或者先锁定
B在锁定A),这样也不会导致死锁的发生。比如按照上面的例子,需要同时锁定两个资源,可以根据资源的
hashcode值大小来判断先后锁定顺序。可以这样改造线程:
public class Thread3 implements Runnable
{
@Override
public void run()
{
try
{
System.out.println("Thread is running");
if ( ThreadResource.resource1.hashCode() >
ThreadResource.resource2.hashCode())
{
//先锁定resource1
synchronized (ThreadResource.resource1)
{
System.out.println("Thread lock resource1");
Thread.sleep(2000);
synchronized (ThreadResource.resource2)
{
System.out.println("Thread lock resource2");
}
System.out.println("Thread release resource2");
}
System.out.println("Thread release resource1");
}
else
{
//先锁定resource2
synchronized (ThreadResource.resource2)
{
System.out.println("Thread lock resource2");
Thread.sleep(2000);
synchronized (ThreadResource.resource1)
{
System.out.println("Thread lock resource1");
}
System.out.println("Thread release resource1");
}
System.out.println("Thread release resource2");
}
}
catch (Exception e)
{
System.out.println(e.getMessage());
}
System.out.println("Thread1 is stop");
}
}
总结:死锁常见于,线程在锁定对象还没释放时,又需要锁定另一个对象,并且此时该对象可能被另一个线程
锁定。这种时候很容易导致死锁。因此在开发时需要慎重使用锁,尤其是需要注意尽量不要在锁里又加锁。
10、线程的分类
java中的线程分为两类:
1.守护线程(如垃圾回收线程,异常处理线程)
2.用户线程(如主线程)
若JVM中都是守护线程,当前JVM将退出。(形象理解,唇亡齿寒)