多线程
目录
概念
区分各个概念
-
程序:一段静态的代码
-
进程:程序的一次执行过程或正在运行的一个程序,是一个动态过程,有生命周期
-
进程是资源分配的单位,系统在运行时为每个进程分配不同的内存区域
-
-
线程:进程可以进一步细化成线程,是一个程序内部的执行路径
-
多线程:同一时间可以执行多个线程
-
线程是调度和执行的最小单位,每个线程有独立的运行栈和程序计数器pc
-
一个进程中的多个线程共享相同的内存单元,他们从同一堆中分配对象,可以访问相同的变量和对象,共享方法区和堆。但是多个线程操作共享的系统资源可能会带来安全隐患
-
-
单核CPU与多核CPU:单核CPU是假的多线程,一个时间单元内还是只能执行一个线程的任务。java的一个程序至少有三个线程,main()主线程、gc()垃圾回收线程、异常处理线程,发送异常会影响主线程
-
并发与并行
-
并行:多个CPU同时执行多个任务
-
并发:一个CPU(采用时间片)同时执行多个任务
-
需要多线程的情况
-
程序需要同时执行多个程序
-
程序需要实现一些需要等待的任务时,比如用户输入、文件读写操作、网络操作、搜索等
-
需要一些后台程序时
创建多线程的方式
方式一 Tread类
/* 创建一个继承于Thread类的子类
* 重写Thread类的run(),将此线程执行的操作声明在run中
* 创建Thread类的子类对象
* 通过此对象调用start()*/
class Mythread extends Thread{
@Override
public void run(){
for (int i=0;i<100;i++){
if(i%2==0) System.out.println(Thread.currentThread().getName()+":"i);//打印当前线程名
}
}
}
public class ThreadTest{
public static void main(String[] args) {
Mythread t1=new Mythread();
t1.start();//启动当前线程+调用当前线程的run方法
for (int i=0;i<100;i++){
if(i%2==0) System.out.println(Thread.currentThread().getName()+":***main***");
}
}
}//结果是两个循环的输出相互交叉,因为是两个线程在跑互不干扰
-
start()的时候应该调父类的run方法,但是子类重写了,因此调用的是重写的这个run()。
-
对于一个线程,只能start一次,对于一个已经start过的线程不能再通过start起一个新的线程
常用的方法:
void start();//启动线程并执行对象的run()方法
run();//线程在被调度时执行的操作
String getName();//返回线程的名称
void setName(String name);//设置该线程名称
static Tread currentTread();//返回当前线程,在Thread子类中就是this,通常用于主线程和Runable实现类
static void yield();//释放当前CPU的执行,释放后各线程竞争
join();//插队,会把插入的线程执行完了才会执行刚刚的线程
boolean isAlive();//判断线程是否还活着
sleep(long millitime);
线程调度
-
对同优先级线程组成先进先出队列,使用时间片策略
-
对高优先级,使用优先调度的抢占式策略
线程的优先级:一般就10级,默认为5
-
MAX_PRIORITY:10
-
MIN_PRIORITY:1
-
NORM_PRIORITY:5
getPriority();//返回线程的优先级
setPriority(int newPriority);//改变线程的优先级
线程创建时继承父线程的优先级,低优先级知识获得调度的概率低,不是一定是在高优先级线程之后才被调用
多线程问题
class Window extends Thread{
//private int ticket=50; 不行,会造成三个线程各执行各的,出现三个ticket总共150个
private static int ticket=50; //加上static后三个线程共用一个ticket,会产生3个50,并发问题。ticket是共享数据
@Override
public void run(){
while(true){
if(ticket>0){
System.out.println(getName()+":票号为:"+ticket);
ticket--;
}else{
break;
}
}
}
}
public class ThreadTest{
public static void main(String[] args) {
Window t1=new Window();
Window t2=new Window();
Window t3=new Window();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
-
显示可能不按顺序,因为显示也需要时间
Thread类本身也实现了Runnable接口
方式二 Runnable接口
/*创建一个实现Runnable接口的类
* 实现类去实现Runnable中的抽象方法run()
* 创建实现类的对象,将此对象作为参数传递到Tread类的构造器中
* 创建Tread类对象,通过Tread类的对象调用start*/
class MThread implements Runnable{
@Override
public void run() {
for (int i=0;i<50;i++){
System.out.println(i);
}
}
}
public class RunnableTest {
public static void main(String[] args) {
MThread m1=new MThread();
Thread t1=new Thread(m1);
t1.start();//调用当前线程的run()-->调用了Runnable类型的target的run()
}
}
和上面一样,存在安全问题
class Window implements Runnable{
private int ticket=50; //可以不加static
@Override
public void run(){
while(true){
if(ticket>0){
System.out.println(Thread.currentThread().getName()+":票号为:"+ticket);
ticket--;
}else{
break;
}
}
}
}
public class ThreadTest{
public static void main(String[] args) {
Window w=new Window();
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();
}
}
开发中,优先选择实现Runnable接口的方式。因为该方式没有类的单继承的局限性,实现的方式更适合来处理多个线程有共享数据的情况
方式三 Callable接口
JDK5.0新增
相比于Runnable,Callable的call()方法相比于run()可以有返回值,方法可以抛出异常,支持泛型的返回值,需要借助FutureTask类获取返回结果
FutureTask,借助了Future接口
-
可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等
-
FutureTast是Future接口唯一的实现类
-
FutureTask同时实现了Runnable、Future接口,它既可以作为Runnable被线程执行,也可以作为Future得到Callable的返回值
class NumThread implements Callable{//创建一个实现Callable接口的实现类
int sum =0;
@Override
public Object call() throws Exception {//实现call方法,将此线程需要执行的操作声明在call方法中
for (int i =0; i < 50; i++){
if(i % 2 == 0){
System.out.println(i);
sum+=i;
}
}
return sum;
}
}
public class ThreadNew1 {
public static void main(String[] args) {//创建Callable接口实现类的对象
NumThread numThread=new NumThread();
FutureTask futureTask=new FutureTask(numThread);
//将此Callable接口实现类的对象作为参数传递到FutureTask构造器中
new Thread(futureTask).start();
//将FutureTasj对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
try {//需要则获取Callable中call方法的返回值
Object sum=futureTask.get();
//get()的返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值
System.out.println("总和为"+sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
-
call()可以有返回值。可以抛出异常,被外面的操作捕获,获取异常信息
-
Callable支持泛型
方式四 线程池
提前创建好多个线程,放入线程池,使用时直接获取,使用完放回池中,可以避免频繁创建销毁、实现重复利用
相关API
-
ExecutorService:真正的线程池接口,常见子类ThreadPoolExecutor
-
Executors:工具类,用于创建并返回不同类型的线程池
线程池提高响应速度(减少了创建线程的时间),降低资源消耗(重复利用线程),便于线程管理
-
corePoolSize:核心池的大小
-
maximumPoolSize:最大线程数
-
keepAliveTime:线程没有任务时最多保持多长时间后终止
class NumberThread implements Runnable{
@Override
public void run() {
for (int i =0; i < 50; i++){
if(i % 2 == 0) System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
public class ThreadNew1 {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(10);//提供指定数量的线程池
service.execute(new NumberThread());//适用于Runnable
//service.submit();适用于Callable
service.shutdown();//关闭线程池
}
}
//System.out.println(service.getClass());得在接口的实现类,利用sevice.getClass()获得实现类名称
ThreadPoolExecutor service1= (ThreadPoolExecutor) service;
service1.setCorePoolSize(10);//就可以设置各种属性
线程的生命周期
一个完整的生命周期
-
新建
-
就绪:start()后,进入线程队列等待CPU时间片,此时已经具备运行条件但是没分配到PCU资源
-
运行:获得CPU资源,run()方法定义了线程的操作和功能
-
阻塞:在特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止执行
-
死亡:完成工作或被提前强制中止或出现异常导致结束
线程的同步
多个线程的执行的不确定性引起执行结果的不稳定,多个线程对账本的共享会造成操作的不完整性,破坏数据
方式一 同步代码块
synchronized(同步监视器){
//需要被同步的代码,即操作共享数据的代码
}
-
同步监视器,就是锁,任何一个类的对象都可以充当锁。但是要求多个线程必须要共用同一把锁,注意代码位置,是不是同一把锁!
-
但是执行速度会变慢。加锁后外面是多线程并行,但是操作同步代码时只能有一个线程参与,相当于单线程
改进上述代码
class Window implements Runnable{
private int ticket=50;
Object obj=new Object();
@Override
public void run(){
//Object obj=new Object();如果放到这里,就不是1个了,每跑一次都有一个锁
while(true){
synchronized (obj) {
if(ticket>0){
try {//利用sleep放大出错的可能性
Thread.sleep(100);//保证了即使线程1在次阻塞了,其他线程也跟着等
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":票号为:"+ticket);
ticket--;
}else{
break;
}
}
}
}
}//修复了错票、重票、负票等问题
public class ThreadTest{
public static void main(String[] args) {
Window w=new Window();
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();
}
}
同一个obj,可以用static
class Window extends Thread{
private static int ticket=50;
private static Object obj=new Object();//不用static,多个线程拿的就不是一个锁
@Override
public void run(){
while(true){
synchronized (obj) {
if(ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":票号为:"+ticket);
ticket--;
}else{
break;
}
}
}
}
}
public class ThreadTest{
public static void main(String[] args) {
Window t1=new Window();
Window t2=new Window();
Window t3=new Window();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
可以用当前对象来代替
class Window implements Runnable{
...
synchronized (this) {......}//this是Window类型的对象w,其他Thread类型全是从w出来的
...
}
public class ThreadTest{...}
class Window extends Thread{
...
//synchronized (this) {......}不对!因为三个线程对应三个Window对象,this代表t1、t2、t3三个对象
synchronized(window.class);//拿当前类充当对象 Class xxx=Window.class,这个是唯一的,因为类只会加载一次
...
}
synchronized如果包多了,比如包上while了,就导致一个线程拿着这个锁一直跑while
方式二 同步方法
如果操作共享数据的代码完整的声明在一个方法中,即可以将此方法声明为同步的
可以将上述while内的代码拎出来写一个完整方法(while不能包进去)
//class Window implements Runnable{}
public synchronized void show(){
if(ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":票号为:"+ticket);
ticket--;
}
}
public static synchronized void show(){//加上static即可,不然没用,此时同步监视器是Window.class
...
}
同步方法仍然涉及到同步监视器,只是不需要显示的声明。
非静态的同步方法,同步监视器是this。非静态的同步方法,同步监视器是当前类本身
单例模式的线程安全
class Bank {//单例模式
private Bank(){}//构造器
private static Bank insatnce=null;
public static Bank getInsatnce(){//懒汉式
if(insatnce==null){
insatnce=new Bank();//这两行对同一对象操作,一个判断一个赋值,会有线程安全问题,需要synchronized
}
return insatnce;
}
}
public static synchronized Bank getInsatnce(){...}//加一个synchronized,或者拿它把if包住
但是这样效率低,因为一个进入后其他线程全需要等待,可以做二重判断
class Bank {//单例模式
private Bank(){}//构造器
private static Bank insatnce=null;
public static Bank getInsatnce(){//懒汉式
if(insatnce==null){
synchronized (Bank.class) {
if(insatnce==null){
insatnce=new Bank();
}
}
}
return insatnce;
}
}
死锁
不同线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。出现死锁后,不会出现异常提示,只是所有线程都处于阻塞状态,无法继续
解决方法:专门的算法、原则,尽量减少同步资源的定义,尽量避免嵌套同步
死锁的实例
public class ThreadTest{
public static void main(String[] args) {
StringBuffer s1=new StringBuffer();
StringBuffer s2=new StringBuffer();
new Thread(){//匿名方式创建线程
@Override
public void run() {
synchronized (s1){//先拿锁s1再拿锁s2
s1.append("a");
s2.append("1");
try {
Thread.sleep(100);//利用sleep加大概率
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2){
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
new Thread(new Runnable() {//匿名方式通过接口创建线程
@Override
public void run() {
synchronized (s2){//先拿锁锁s2再拿锁s1
s1.append("c");
s2.append("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1){
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
}//程序运行后有几率锁死,无法继续运行下去
方式三 Lock锁
JDK5.0加入,通过显式定义同步锁对象来实现同步,同步锁使用Lock对象充当
上述抢票代码做修改
class Window implements Runnable{
private int ticket=50;
private ReentrantLock lock=new ReentrantLock();//声明一个ReentrantLock类的对象
@Override
public void run() {
while (true){
try {
lock.lock();//调用lock(),下面try内的代码就类似同步代码块内,单线程
if(ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":票号为:"+ticket);
ticket--;
}else break;
}finally {
lock.unlock();//解锁
}
}
}
}
-
如果是继承的方式创建多线程,记得给lock定义的时候加个static
synchronized和Lock
-
都是解决线程安全的问题
-
Lock需要手动的启动同步(lock()方法),结束同步(unlock()方法)也要手动实现。synchronized机制是在执行完相应的同步代码以后,自动释放同步监视器
-
Lock只有代码块锁,synchronized有代码块锁和方法锁。但是使用Lock锁JVM花费更少的时间来调度线程,性能更好,且扩展性更好(有更多的子类)
线程的通信
交错打印
class Number implements Runnable{
private int number=1;
@Override
public void run() {
while (true){
synchronized (this) {
notify();//notify和notifyAll在此处都一样,因为只睡了一个。
//后一个线程执行到此处时唤醒上一个被wait的线程,但是后一线程拿着这个锁,从而实现交错
if (number <= 30){
System.out.println(Thread.currentThread().getName()+":"+number);
number++;
try {
wait();//使得调用如下wait方法的线程进入阻塞状态
} catch (InterruptedException e) {
e.printStackTrace();
}
}else break;
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
-
wait():一旦执行次方法,当前线程会进入阻塞状态,并释放同步监视器
-
notify():一旦执行,会唤醒被wait的一个线程,如果有多个线程wait,就唤醒优先级高的线程
-
notifyAll():一旦执行会唤醒所有被wait的线程
-
线程通信中:上面三个方法的必须使用在同步代码块或同步方法中,调用者必须是同一个同步监视器
Object obj=new Object();
...
synchronized (obj) {
this.notify();//和synchronized不是一个同步监视器,会报错:IllegalMonitorStateException
...
三个方法是定义在java.lang.Object类中
sleep()和wait()
都让线程进入阻塞状态
不同L
-
声明位置不同:Thread类中声明sleep(),Object类中声明wait()
-
调用范围不同:sleep()可以在任何场景下调用,而wait()只能在同步代码块或同步方法中使用
-
对于同步监视器:如果二者都使用在同步代码块/同步方法中,sleep()不会释放锁,wait()会释放锁
生产者/消费者问题
class Clerk{
private int productCount=0;
public synchronized void produceProduct() {
if(productCount<20){
productCount++;
System.out.println(Thread.currentThread().getName()
+":生产第"+productCount+"个产品");
notify();
}else{
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void consumeProduct() {
if(productCount>0){
System.out.println(Thread.currentThread().getName()
+":消费第"+productCount+"个产品");
productCount--;
notify();
}else{
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Producer extends Thread{
private Clerk clerk;
public Producer(Clerk clerk){
this.clerk=clerk;
}
@Override
public void run() {
System.out.println(getName()+":生产产品");
while (true){
try {
sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.produceProduct();
}
}
}
class Consumer extends Thread{
private Clerk clerk;
public Consumer(Clerk clerk){
this.clerk=clerk;
}
@Override
public void run() {
System.out.println(getName()+":消费产品");
while (true){
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.consumeProduct();
}
}
}
public class ProductTest {
public static void main(String[] args) {
Clerk clerk=new Clerk();
Producer p1=new Producer(clerk);
p1.setName("生产者");
Consumer c1=new Consumer(clerk);
c1.setName("消费者1");
Consumer c2=new Consumer(clerk);
c2.setName("消费者2");
p1.start();
c1.start();
c2.start();
}
}