Java 多线程基础

一、线程基础

  1. 线程概述:
    线程是程序运行的基本执行单元,当操作系统执行一个程序时,会在系统中建立一个进程,而在这个进程中,必须建立一个线程(这个线程称为主线程)来作为程序的主入口点,进程和线程是操作系统中必不可少的运行模型,在操作系统中可以有多个进程,包括系统进程和用户进程,一个进程可有多个线程,进程和进程之间不可共享内存,线程不仅可以共享进程的内存,而且还拥有自己的内存空间,这段内存空间也叫线程栈,主要用来保存线程内部所使用的数据,如线程执行的函数中所定义的变量。
  2. 线程的意义:
    (1)充分利用CPU资源
    (2)简化编程模型
    (3)简化异步事件的处理
    (4)使GUI更有效率
    (5)提高程序的执行效率

二、创建线程

Java语言使用类Therad代表线程,所有的线程对象必须是Therad类或其子类的实例,每条线程的作用是完成一定的任务,,实际上就是执行一段程序流,Java中使用run()方法来表示这一段程序流,创建线程有两种方法一是使用Therad类创建,二是使用Runnable接口创建。

  1. 使用使用Therad类创建线程
//创建线程所需要的类.第一种方法,继承thread类,重写run()方法,该类再调用run()方法
class Xc extends Thread {
	public void run() {//重写run方法
		for(int i=0;i<10;i++) {
			System.out.println("线程函数"+i);	
		}
	}
}
public class mythread{
	public static void main(String[] args) {
	Xc xc1=new Xc();//线程1
	xc1.start();//开始线程1运行
	Xc xc2=new Xc();//线程2
	xc2.start();//开始线程2运行
	}
}

运行结果
在这里插入图片描述

虽然线程类的子类可以实例化,但是在子类中必须覆盖run()方法才能运行线程的代码,因为Thread类实现了Runnable接口,需要重写其中Runnable的run方法

  1. 使用Runnable接口创建线程
//第二种方法,实现runnable接口,实现run()方法,创建thread对象,调用start方法
class Xc1 implements Runnable{
	public void run() {
		for(int i=0;i<=10;i++) {
			System.out.println("线程函数"+i);	
		}
	}
}
public class mythread01 {
	public static void main(String[] args) {
		Xc1 a1 =new Xc1();
		Xc1 a2 =new Xc1();
		Thread a= new Thread(a1);
		Thread b= new Thread(a2);//将实例化对象传入线程a,b
		a.start();
		b.start();
	}
}

运行结果
由此可见两个线程同时运行
由此可见两个线程同时运行
在使用Runnable接口的类使,必须使用Therad类的实例才能创建线程,使用Runnable接口创建线程的两个步骤:将Runnable接口实例化;创建Therad对象,并将第一步实例化的对象作为参数传入Therad类的构造方法,最后通过Therad类的start()方法运行线程。

三、线程的常用方法

  1. static Thread currentTherad():Therad类的静态方法,返回当前正在执行的线程对象
  2. String getName();获取线程名
  3. void setName(String name);设置线程名
  4. long getId();获取该线程的标识符
  5. void join();线程的插队行为,等待该线程终止才能继续执行
  6. boolean isAlive();线程是否处于运行状态,处于运行状态时返回true,否则返回false,
  7. static void run();线程执行的操作内容
  8. static void sleep(long mills);线程睡眠,会抛出异常
  9. void start();使线程处于就绪状态,等待CPU调度
  10. static void yield();暂停当前正在执行的线程对象,并执行其他线程,从运行状态回到可运行状态
public class MyThread03 extends Thread{
	public void run(){
		System.out.println(this.getName());//获取线程名
	}
	public static void main(String[] args) {
		System.out.println(Thread.currentThread().getName());//获取当前正在执行的线程对象
		MyThread03 thread1=new MyThread03();
		System.out.println("isAlive:"+thread1.isAlive());//返回线程是否处于运行状态
		MyThread03 thread2=new MyThread03();
		MyThread03 thread3=new MyThread03();//创建3个线程
		thread1.setName("线程1");
		thread2.setName("线程2");//设置线程1.2的名字
		thread1.start();
		thread2.start();
		thread3.start();
	}
}

运行结果
在这里插入图片描述
第一个输出的是主函数的线程名,第二行表示线程thread1已不在处于运行状态,由于thread3没有命名线程名称,所有系统默认命名为Thread-2

四、线程的生命周期

线程要经历新建(new),就绪(Rnnnable),运行(Running),阻塞(Blocked),结束(Dead)五种不同的状态,这5种状态通过线程类中的方法来进行控制。一旦线程开始执行run()方法,就会一直到这个方法结束之后线程才退出。这是一种终止线程的方法,其他的是stop()方法,强行终止线程(这个方法不推荐),suspend()(不建议使用)使用后挂起线程,可以通过resume()(不建议使用)方法唤醒线程,而使用sleep(long millis)方法使线程休眠后,只能在设定时间结束处于就绪状态,该方法一般要抛出异常,join()该方法为强制执行线程,表示执行该线程结束后才能执行其他线程。
请记住下面的图!!!
在这里插入图片描述

public class MyThread04 extends Thread{
	public void run() {
		for(int x=0;x<5;x++) {
			try {
				Thread.sleep(600);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			System.out.println("子线程number:"+x);
		}
	}
	public static void main(String[] args) throws InterruptedException {
		Thread mainThread =Thread.currentThread();//获得主线程
		MyThread04 thread =new MyThread04();
		thread.start();//开始子线程
		
		for(int x=0;x<10;x++) {
			if(x==5) {      //设置强制执行条件
				try {
					mainThread.join();//强制执行线程任务
				}catch(InterruptedException e) {
					e.printStackTrace();
				}
			}
			try {
				Thread.sleep(100);//延缓执行
				
			}catch(InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+"执行,x="+x);
		}
	}
}

运行结果
在这里插入图片描述
本程序启动了两个线程main线程和子线程,本来两个程序交替运行,而当满足x==5时main线程会强制执行完毕后会再继续执行子线程,本来main线程要执行10次,遇到了join()方法,被强制执行完毕。

五、线程的同步与死锁

程序利用线程可以进行更为高效的程序处理,如果在没有多线程的程序中,那么一个程序在处理某些资源时主线程会进行全部处理,但是这样的处理速度会很慢,但是如果使用多线程的处理机制,可以使子线程与主线程一起处理资源,那么效率会比一个线程执行的更高。虽然使用多线程同时处理资源效率比单线程高许多,但是多线程如果操作同一个资源时一定会存在一些问题,例如操作资源完整性问题,由此有线程的同步与死锁问题。

  1. 线程同步
    线程同步是指若干个线程对象并行进行资源访问时实现资源处理的保护操作,下面利用一个模拟买票的程序来进行同步问题的说明。
class MyThread implements Runnable{//定义线程执行类
	private int ticket =3;//总票数为3
	public void run() {
		while(true) {//持续买票
			if(this.ticket>0) {//还有剩余票
			try {
				Thread.sleep(100);//模拟网络延迟
			}catch(InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+"买票,ticket ="+this.ticket--);
			}else {
				System.out.println("****票已经卖光了****");
			break;//跳出循环
			}
		}
	}
}
public class MyThread05 {
	public static void main(String[] args) throws Exception{
	MyThread mt=new MyThread();
	new Thread(mt,"售票员A").start();//开启买票线程
	new Thread(mt,"售票员B").start();
	new Thread(mt,"售票员C").start();
	}
}

运行结果
在这里插入图片描述

在本程序中为了更好地观察到同步问题,在判断票数(this.ticket>0) 和卖票(this.ticket–) 操作之间追加了一个线程休眠操作以实现延迟的模拟。通过执行的结果也可以发现程序出现了不同步的问题,而造成这些问题主要是由于代码的操作结构所引起的,因为卖票操作分为两个步骤。

步骤1 ( this.ticket>0):判断票数是否大于0,大于0则表示还有票可以卖。

步骤2 (thisticket–); 如果票数大于0,则卖票出去。

假设现在只剩下最后一张票了,当第一个线程满足售票条件后(此时并未减少票数),其他的线程也有可能同时满足售票的条件,这样同时进行自减操作时就有可能造成负数。

线程同步处理

造成并发资源访问不同步的主要原因在于没有将若干个程序逻辑单元进行整体性的锁定,即当判断数据和修改数据时只允许一个线程进行处理,而其他线程需要等待当前线程执行完毕后才可以继续执行,这样就使得在同一个时间段内,只允许一个线程执行操作, 从而实现同步的处理。

Java中提供有synchronized关键字以实现同步处理,同步的关键是要为代码加上“锁",而对于锁的操作程序有两种:同步代码块、同步方法。

同步代码块是指使用synchronized 关键字定义的代码块,在该代码执行时往往需要设置一个同步对象,由于线程操作的不确定状态,所以这个时候的同步对象可以选择this。

class MyThread implements Runnable{//定义线程执行类
	private int ticket =3;//总票数为3
	public void run() {
		while(true) {//持续买票
			synchronized(this) {
				if(this.ticket>0) {//还有剩余票
					try {
						Thread.sleep(100);//模拟网络延迟
					}catch(InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName()+"买票,ticket ="+this.ticket--);
				}else {
					System.out.println("****票已经卖光了****");
					break;//跳出循环
				}
			}
		}
	}
}
public class MyThread05 {
	public static void main(String[] args) throws Exception{
	MyThread mt=new MyThread();
	new Thread(mt,"售票员A").start();//开启买票线程
	new Thread(mt,"售票员B").start();
	new Thread(mt,"售票员C").start();
	}
}

运行结果
在这里插入图片描述
本程序将票数判断与票数自减的两个控制逻辑放在了一个同步代码块中, 当进行多个线程并发执行时,只允许有一个线程执行此部分代码,就实现了同步处理操作。

提示:同步会造成处理性能下降

同步操作的本质在于同一个时间段内只允许有一个线程执行, 所以在此线程对象未执行完的过程中其他线程对象将处于等待状态,这样就会适成程序处理性能的下降。但是同步也会带来些优点:数据的线程访问安全。

  1. 线程死锁
    当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步。
    这时容易引发一种现象:程序出现无限等待,这种现象我们称为死锁。
    资源共享时需要进行同步操作,程序中过多的同步会产生死锁。
    解释:死锁一般情况下就是表示在互相等待,是在程序运行时出现的一种问题,这种情况能避免就避免掉。

    synchronzied(A锁){
    
    	synchronized(B锁){
    
    	}
    }
    
  2. ReentrantReadWriteLock 读写锁(乐观锁,无罪假设)

ReentrantLock是排他锁,排他锁在同一时刻仅有一个线程可以进行访问,实际上独占锁是一种相对比较保守的锁策略,在这种情况下任何“读/读”、“读/写”、“写/写”操作都不能同时发生,这在一定程度上降低了吞吐量。然而读操作之间不存在数据竞争问题,如果”读/读”操作能够以共享锁的方式进行,那会进一步提升性能。因此引入了ReentrantReadWriteLock,顾名思义,ReentrantReadWriteLock是Reentrant(可重入)Read(读)Write(写)Lock(锁),我们下面称它为读写锁。

读写锁内部又分为读锁和写锁,读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。读锁和写锁分离从而提升程序性能,读写锁主要应用于读多写少的场景。


import java.util.concurrent.locks.ReentrantReadWriteLock;
//线程的死锁,线程独占资源
public class Text2 {
    public static void main(String[] args) {
        My2 target = new My2();
        Thread t = new Thread(target, "1号窗口:");
        Thread t2 = new Thread(target, "2号窗口:");
        Thread t3 = new Thread(target, "3号窗口:");
        Thread t4 = new Thread(target, "4号窗口:");
        t.start();
        t2.start();
        t3.start();
        t4.start();
    }
}
class My2 implements Runnable {
    int sum = 10;//定义票数
    static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
    @Override
    public void run() {
        while (true) {
            // t t2 t3 t4都要开门,t有钥匙,进来了出去后,t2再开门干活再锁门
            // synchronized (this) {
            lock.writeLock().lock();
            // sum=1时 t t2 t3 t4都进来
            try {
                if (sum > 0) {
                    try {
                        // t t2 t3 t4都睡了
                        Thread.sleep(100);//模拟网络延迟
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    // t醒了 sum=1时,sum-- = 1,sum=0
                    // t2醒了 sum=0时,sum-- = 0,sum=-1
                    // t3醒了 sum=-1时,sum-- = -1,sum=-2
                    // t4醒了 sum=-2时,sum-- = -2,sum=-3
                    System.out.println(Thread.currentThread().getName() + sum--);
                }
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }finally {
                lock.writeLock().unlock();//防止死锁,会自动释放,不释放就独占报错了
            }
        }
    }
}

运行结果
在这里插入图片描述

  1. 两种方式的区别
    需要注意的是,用sychronized修饰的方法或者语句块在代码执行完之后锁会自动释放,而是用Lock需要我们手动释放锁,所以为了保证锁最终被释放(发生异常情况),要把互斥区放在try内,释放锁放在finally内!

与互斥锁相比,读-写锁允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程)从理论上讲,与互斥锁定相比,使用读-写锁允许的并发性增强将带来更大的性能提高。

六、多线程面试题

sleep、wait、yield、join的区别:

1.sleep:属于thread类,sleep过程中不释放锁,造成线程堵塞,通常要抛出异常,但是依然保持监控状态,睡眠时间到回到可运行状态,可中断,sleep给其他线程运行时不考虑线程的优先级,因此会给优先级的线程以运行机会。

2.wait:属于object类,wait过程中会释放对象锁,只有当其他线程调用notify才能唤醒此线程,由此可知,wait使用时必须先获取对象锁,即在synchronized修饰的代码块中使用

3.yield:属于thread类,不会释放锁,和sleep不同的是,不会造成线程堵塞,让线程重回可运行状态,yield方法只能是相同优先级或者更高优先级的线程有执行机会

  1. join:此方法一般用于等待异步线程执行完结果之后才能继续执行,相当于插队行为,等待调用join方法的线程结束后,此时当前线程才继续执行。

创建线程的三种方式

  1. 继承Thread类,重写run方法;
  2. 实现Runnable接口,重写run方法,再将任务传入Thread类
  3. 创建FutureTask为了任务类,传入Callable对象,重写call方法,再将任务传入Thread类

注:线程的运行类只有通过Thread类的start方法启动,所有的任务父接口为Runnable接口,Thread类也是实现类Runnable接口,不过该接口的run方法为void无返回,jdk8之后增强了Callable接口,任务为call方法可以对执行任务进行获取返回,通过FutureTask对象实现RunnableFuture接口从而实现Runnable接口,达到了任务声明也体现了接口的规范性。

synchronized加静态方法和成员方法上有什么区别?

synchronized本质是一种独占锁,即某一时刻仅能有一个线程进入synchronized方法,其他线程必须等待,处于block状态,修饰静态方法表示锁住的是.class对象,修饰成员方法锁住的是调用该方法的某个对象

sleep()和wait()有什么区别?

  1. sleep() 是 Thread 类的静态本地方法;wait() 是Object类的成员本地方法
  2. sleep() 方法可以在任何地方使用;wait() 方法则只能在同步方法或同步代码块中使用,否则抛出异常Exception in thread “Thread-0” java.lang.IllegalMonitorStateException
  3. sleep() 会休眠当前线程指定时间,释放CPU资源,不释放对象锁,休眠时间到自动苏醒继续执行;wait() 方法放弃持有的对象锁,进入等待队列,当该对象被调用 notify() / notifyAll() 方法后才有机会竞争获取对象锁,进入运行状态

如果线程池的线程满了的话(线程池里面所有的线程都在工作),再往里面塞任务,会怎么做?它会再往哪个位置去存放这些东西?或者做一些其他处理?有什么一些策略?

当任务提交给线程池时,首先会判断是否有空闲线程,如果有的话直接交给空闲线程工作,如果核心线程全部在工作,则新加的任务会放入任务阻塞对了里面,如果任务队列也满了,则会继续创建线程来工作,直至达到线程池的最大线程数,这时新加入的任务会遭到线程池的拒绝,拒绝策略有
AbortPolicy:默认的拒绝策略,丢弃任务并且抛出异常
DiscardPolicy:丢弃任务并且不抛出异常
DiscardOldestPolicy:丢弃最老的任务(任务队列的前面), 然后重新提交被拒绝的任务
CallerRunsPolicy:返回任务提交者进行任务的处理

线程有哪几种状态?

五种状态:创建(new),就绪(Runnable),运行(Running),销毁(Dead),堵塞(Blocked)

堵塞状态的情况有:1)等待阻塞,当线程调用wait方法的时候,
       2)同步阻塞, 当线程未拿到同步锁的时候。
       3)其他阻塞,运行sleep或者join方法的时候。

当一个线程进入一个对象的一个 synchronized 方法后,其它线程是否可进入此对象的其它方法?

  1. 其他方法前是否加了 synchronized 关键字,如果没加,则能。
  2. 如果这个方法内部调用了 wait,则可以进入其他 synchronized 方法。
  3. 如果其他个方法都加了 synchronized 关键字,并且内部没有调用 wait,则不能。
  4. 如果其他方法是 static,它用的同步锁是当前类的字节码,与非静态的方法不能同步,因为非静态的方法用的是 this,综上所述,这种也不能。

在这里插入图片描述
D 错,返回当前正在执行的线程的对象的引用
在这里插入图片描述
错误的是ABC ,启动start方法时使线程处于可运行状态,但是需要等待CPU的调度才能运行
notify()唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。
wait方法使线程处于等待状态,但是会释放对象锁,属于object类的方法
在这里插入图片描述
wait方法使线程处于等待状态,此对象发出notify方法时线程进入可运行状态,而不是运行状态
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
star方法使线程处于可运行状态,run方法表示线程正在处于运行状态,run方法结束,线程生命周期结束,使用yield方法会退出线程重回可运行状态

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值