JavaSE高级开发之多线程(上)
进程知识在面试中占到80%的问题!!
进程与线程的概念
进程:操作系统中一个程序的执行周期称为进程。
线程:一个进程执行多个任务。通常来讲,每一个任务就称为一个线程。
DOS系统属于单进程,许多嵌入式系统也是单进程。
进程与线程的比较:
1.与进程相比,线程更加轻量级,创建,撤销一个线程比启动。撤销一个进程开销小得多。一个进程中的所有线程共享此进程的所有资源。
2.没有进程就没有线程,进程一旦终止,其内的线程也将不复存在。
3.进程是操作系统资源调度的基本单位,进程可以独享资源;
线程需要依托进程提供的资源,无法独立申请操作系统资源,是OS任务执行的基本单位。
**高并发:**同一时间段进程的线程访问量非常高。(由于线程过多,开销很大,因此需要部署多个服务器,就被称为分布式。)
高并发带来的问题:服务器内存不够用,无法处理新的请求。
DDos攻击:黑客利用僵尸设备同时对某个进程同时发起请求,因此占用正常用户的访问请求。
线程的五个状态:
创建:
就绪状态:此时没能获取到CPU的时间片段,因为同一时间内单核CPU只能处理一个任务
运行状态:
阻塞状态:进入阻塞状态的线程不能直接运行,必须再次进入就绪状态争取时间片段
终止:
Java多线程的实现:
1.继承Thread类实现多线程
java.lang.Thread是线程操作的核心类。创建一个线程最简单的方法就是直接继承Thread类,然后覆写run()方法(相当于主线程的main()方法),是线程的入口方法。
具有多继承的局限性,业务逻辑写在run方法中,会与进程类耦合,灵活性低,复用性小,违背了开闭原则。
无论哪种方法实现多线程,线程启动一定调用Thread类提供的start方法,而不是run()方法
线程start()方法只能调用一次,多次调用会抛异常java.lang.IllegalThreadStateException
线程调用的顺序于优先级有关。
通过构造方法给Thread重命名:
public Thread(String name) {
init(null, null, name, 0);
}
获取线程名:线程对象名.getName();
重命名:线程对象名.setName("xxx");
class MyThread extends Thread{
String name;
public MyThread(String name) {
this.name=name;
}
public void run(){
for(int i=0;i<10;i++){
System.out.println(this.name+i);
}
}
}
public class ThreadTest {
//主线程
public static void main(String[] args) {
MyThread myThread = new MyThread("线程1");
MyThread myThread1 = new MyThread("线程2");
MyThread myThread2 = new MyThread("线程3");
myThread.start();
myThread2.start();
myThread1.start();
}
}
当调用start方法(Java)时,会判断当前线程是否新创建的且未被调用过,然后进入start0(Java)方法,进行资源调度,系统分配(JVM)会调用到 JVM_StartThread 方法,利用本地方法创建线程(JVM),然后回调run(JAVA的方法)执行线程的具体操作任务
2.实现Runnable接口
因为Thread是类,因此具有单继承局限,因此引入Runnable接口
是代理模式的实现逻辑,因为Thread类也实现了Runnable接口,通过构造方法将Runnable接口对象传给Thread类public Thread(Runnable target)
,调用start方法创建多线程。
使用Runnable接口实现多线程的方法可以实现业务逻辑的复用
public static void main(String[] args) {
MyThread1 myThread = new MyThread1("线程1");
MyThread1 myThread1 = new MyThread1("线程2");
MyThread1 myThread2 = new MyThread1("线程3");
//1.通过Thread的构造方法执行
new Thread(myThread).start();
new Thread(myThread1).start();
new Thread(myThread2).start();
//2.匿名内部类
Runnable runnable=new Runnable() {
@Override
public void run() {
System.out.println("hello,new thread");
}
};
new Thread(runnable).start();
//3.lambda表达式
new Thread(
()-> System.out.println("hello new thread")
).start();
}
3.Thread与Runnable接口的区别
因为Thread是类,因此具有单继承局限
在开发之中使用Runnable还有一个特点:使用Runnable实现的多线程的程序类可以更好的描述出程序共享的概念
使用Thread实现数据共享(产生若干线程进行同一数据的处理操作)
class Thread3 extends Thread{
private static int tick=10;
@Override
public void run() {
while(this.tick>0){
System.out.println(this.getName()+"剩余:"+this.tick--+"张票");
}
}
}
public static void main(String[] args) {
//利用Thread实现数据共享
Thread3 thread1=new Thread3();
thread1.setName("Thread-A");
Thread3 thread2=new Thread3();
thread2.setName("Thread-B");
thread1.start();
thread2.start();
//利用Runnable实现
Runnable runnable=new Runnable() {
private int ticket=10;
@Override
public void run() {
while(ticket>0){
//Thread.currentThread()获取当前线程名
System.out.println(Thread.currentThread()+ "剩余:"+ticket--+"张票");
}
}
};
new Thread(runnable,"线程一").start();
new Thread(runnable,"线程二").start();
}
4.Callable实现多线程
Runnable中的run()方法没有返回值,它的设计也遵循了主方法的设计原则:线程开始了就别回头。但是很多时候 需要一些返回值,例如某些线程执行完成后可能带来一些返回结果,这种情况下就只能利用Callable来实现多线程。
Callable中的抽象方法为call(),并且含有返回值,需要通过Future接口的实现类FetureTask类接收Callable对象获得线程执行后的返回值,并且FetureTask类还实现了Runnable接口。
通过Thread类的构造方法接收FetureTask对象启动线程
FetureTask类构造方法 public FutureTask(Callable<V> callable)
超过指定时间则跳过结果(抛异常)继续执行,不会一直等待public V get(long timeout, TimeUnit unit)
获得返回结果public V get()
public class CallableTest implements Callable<String> {
private int tick=10;
@Override
public String call() throws Exception {
while(this.tick>0){
System.out.println(Thread.currentThread().getName()+"剩余:"+this.tick--+"张票");
}
return "票卖完了";
}
public static void main(String[] args) {
Callable<String> callable=new CallableTest();
FutureTask<String> futureTask=new FutureTask<String>(callable);
Thread thread = new Thread(futureTask);
thread.start();
try {
//获取任务返回结果(任务结束)
// String result= futureTask.get();
//超过一秒未返回结果则抛出异常
String result= futureTask.get(1,TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
System.out.println("结束");
}
}
主方法本身就是一个线程,所有的线程都是通过主线程创建并启动的。
线程方法体系图:
sleep方法
线程休眠:指的是让线程暂缓执行一下,等到了预计时间之后再恢复执行。
线程休眠会交出CPU,让CPU去执行其他的任务。但是有一点要非常注意,sleep方法不会释放锁,也就是说如果 当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象
休眠时间使用毫秒作为单位
public static native void sleep(long millis) throws InterruptedException
Runnable runnable = new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+ "执行时间" + LocalDateTime.now());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
new Thread(runnable,"线程2").start();
new Thread(runnable,"线程3").start();
}
所有的代码是依次进入到run()方法中的。
真正进入到方法的对象可能是多个,也可能是一个。进入代码的顺序可能有差异,但是总体的执行是并发执行。
线程让步(yield()方法
暂停当前正在执行的线程对象,并执行其他线程,yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield什么时候让是不确定的,因此整个执行过程是随机的。另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。
调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时 间,这一点是和sleep方法不一样的。
public static void main(String[] args) {
//yiled方法 让当前线程回到就绪状态
Runnable runnable= () -> {
for(int i=0;i<3;i++){
Thread.yield();
System.out.println(Thread.currentThread().getName()+"i="+i);
}
};
new Thread(runnable,"线程一").start();
new Thread(runnable,"线程三").start();
new Thread(runnable,"线程二").start();
}
join()方法
在一个线程中调用其他线程对象的join方法,会阻塞当前线程,去执行其他线程的run方法,执行完毕后再开始执行当前线程。
class MyRunnable implements Runnable{
private int tick=1000;
@Override
public void run() {
while(tick>0){
System.out.println(Thread.currentThread().getName()+"tick="+tick--);
}
}
}
public class ThreadMethodJoin {
public static void main(String[] args) {
MyRunnable runnable=new MyRunnable();
//线程
Thread thread=new Thread(runnable,"thread-A");
thread.start();
//在主线程中调用线程的join方法会阻塞主线程,直到调用线程对象的run方法执行完毕,主线程才会继续执行
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前线程"+Thread.currentThread().getName());//主线程
}
}
线程停止
多线程中由三种方法停止线程
1.设置标记位
2.使用stop方法强制使线程退出,但是不安全已被废弃
public class FlagTest {
public static void main(String[] args) throws InterruptedException {
//使用标记位方式停止线程
Runnable runnable=new MyRunnable();
Thread thread = new Thread(runnable, "标记线程");
thread.start();
Thread.sleep(3000);//主线程休眠3秒
//修改标记位
((MyRunnable) runnable).setFlag(false);
//使用stop强制停止
// thread.stop();
}
}
class MyRunnable implements Runnable{
private boolean flag=true;
@Override
public void run() {
int i=0;
while(this.flag){
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"第"+ ++i+"次执行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
3.使用Thread类的方法interrupt中断线程。
public class InterruptTest {
public static void main(String[] args) throws InterruptedException {
Runnable runnable=new MyRunnable2();
Thread thread = new Thread(runnable, "标记线程");
thread.start();
Thread.sleep(3000);//主线程休眠3秒
thread.interrupt();
}
}
class MyRunnable2 implements Runnable{
@Override
public void run() {
int i=0;
while(true){
try {
//判断线程的中断情况
boolean interupt=Thread.currentThread().isInterrupted();
//非阻塞情况
if(interupt){
break;
}
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"第"+ ++i+"次执行");
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().isInterrupted());
e.printStackTrace();
return;
}
}
}
}
interrupt() 方法只是改变中断状态而已,它不会中断一个正在运行的线程。
如果,线程的当前状 态处于非阻塞状态,那么仅仅是线程的中断标志被修改为true而已;如果线程的当前状态处于阻塞状态,那么在将 中断标志设置为true后,还会有如下三种情况之一的操作:
如果是wait、sleep以及jion三个方法引起的阻塞,那么会将线程的中断标志重新设置为false,并抛出一个 InterruptedException;此时可以通过异常捕获,决定是否退出线程
线程优先级
线程的优先级指的是,线程的优先级越高越有可能 先执行,但仅仅是有可能而已
- 高优先级:public final static int MAX_PRIORITY = 10;
- 中等优先级:public final static int NORM_PRIORITY = 5; (主线程优先级为5)
- 低优先级:public final static int MIN_PRIORITY = 1;
设置优先级:public final void setPriority(int newPriority)
线程是有继承关系的,比如当A线程中启动B线程,那么B和A的优先级将是一样的。
守护线程
java 中有两种线程:用户线程和守护线程。守护线程是一种特殊的线程,它属于是一种陪伴线程。只要当前JVM进程中存在任何一个非守护线程没有结束,守护线程就在工作;只有当后 一个非守护线程结束时,守护线程才会随着JVM一同停止工作。
主线程main是用户线程,JC垃圾回收器是一种守护线程
设置守护线程public final void setDaemon(boolean on)
设置线程A为守护线程,此语句必须在start方法之前执行
public class ShouHuThread {
public static void main(String[] args) {
//主线程是用户线程
//System.out.println(Thread.currentThread().isDaemon());//falase
Thread thread = new Thread(new DeamonRunn(), "线程-A");
//必须在start前调用
thread.setDaemon(true);
thread.start();
Thread thread1 = new Thread(new DeamonRunn(), "Thread-B");
thread1.start();
//主线程休眠
try {
Thread.sleep(3000);
//B是用户线程,还有主线程是用户线程,当所有用户线程都终止守护线程才会终止
thread1.interrupt();
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//主线程
}
}
class DeamonRunn implements Runnable{
@Override
public void run() {
int i=0;
while(true){
System.out.println(Thread.currentThread().getName()+"调用第"+ ++i+"次");
try {
System.out.println("线程名称:" + Thread.currentThread().getName()
+ ",i=" + i + ",是否为守护线程:"
+ Thread.currentThread().isDaemon());
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("线程名称:" + Thread.currentThread().getName() + "中断线程 了");
e.printStackTrace();
}
}
}
}
线程的同步与死锁
由于每一个线程轮番抢占资源而引起的问题,由于多个线程可以同时进入共享资源,但是由于共享资源有限,线程虽然能同时进入共享资源,但是由于出来的时间不一致,有可能导致资源已经消耗殆尽而还有线程在享用.
解决方案:为这个共享资源"上一把锁",保证同一时间只有一个线程进入共享资源.
1.同步代码块
直接将共享资源放入synchronized(this){ }代码块中,表示同一时刻只有一个线能够进入同步代码块,但是多个线程可以同时进入run方法,只有一个线程能够进入synchronized代码块内
public class Block {
public static void main(String[] args) {
Runnable runnable=new MyThread();
Thread thread=new Thread(runnable,"黄牛1");
Thread thread1=new Thread(runnable,"黄牛2");
Thread thread2=new Thread(runnable,"黄牛3");
thread.start();
thread1.start();
thread2.start();
}
}
class MyThread implements Runnable{
private int tick=100;
@Override
public void run() {
for(int i=0;i<100;i++){
//同步代码块
synchronized (this){
if(tick>0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"还剩"+this.tick--+"张票");
}
}
}
}
}
2.同步方法
将共享资源放入一个加锁的方法中(在方法声明中加synchronized),在run方法中调用此共享方法,表示此时只有一个线程能够进入同步方法.
public class Method {
public static void main(String[] args) {
Runnable runnable=new MyThread();
Thread thread=new Thread(runnable,"黄牛1");
Thread thread1=new Thread(runnable,"黄牛2");
Thread thread2=new Thread(runnable,"黄牛3");
thread.start();
thread1.start();
thread2.start();
}
}
class MyThread1 implements Runnable{
private int tick=100;
@Override
public void run() {
for(int i=0;i<100;i++){
this.sale(tick);
}
}
public synchronized void sale(int tick){
if(tick>0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"还剩"+this.tick--+"张票");
}
}
}
synchronized上锁会隐式解锁
synchronized对象锁概念
1.synchronized(this)以及普通synchronized方法,只能防止多个线程同时执行同一个对象的同步段,synchronized锁的是括号中的对象,而非代码
public class ObjSynchronized {
public static void main(String[] args) {
Syn syn=new Syn();
for(int i=0;i<3;i++){
Runnable runnable=new MyRunnable(syn);
Thread thread=new Thread(runnable);
thread.start();
}
}
}
class MyRunnable implements Runnable{
private Syn syn;
public MyRunnable(Syn syn){
this.syn=syn;
}
@Override
public void run() {
this.syn.test();
}
}
class Syn{
public synchronized void test(){
System.out.println(Thread.currentThread().getName()+ "方法开始");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+ "方法结束");
}
}
2.全局锁:锁代码段.
1使用类的静态同步方法
synchronized与static一起使用,此时锁的是当前使用的类而非对象
class Syn1{
public static synchronized void test(){
System.out.println(Thread.currentThread().getName()+ "方法开始");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+ "方法结束");
}
}
2.在代码块中锁当前class对象
synchronized(类名称.class){}
class Syn2{
public synchronized void test(){
synchronized (Syn.class){
System.out.println(Thread.currentThread().getName()+ "方法开始");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+ "方法结束");
}
}
}
线程获得对象锁时可以获取对象的所有同步方法,因此对象的所有同步方法都会上锁,不能再有其他线程进入
public class SynAply implements Runnable{
private Sync sync;
public SynAply(Sync sync){
this.sync=sync;
}
@Override
public void run() {
//因为synchronized是对象锁,因此只能有一个对象进入当前方法
sync.testA();
sync.testB();
}
public static void main(String[] args) {
Sync sync=new Sync();
Runnable synAply=new SynAply(sync);
Thread threadA=new Thread(synAply,"A");
Thread threadB=new Thread(synAply,"B");
threadA.start();
threadB.start();
}
}
class Sync{
public synchronized void testA(){
//只有名为A的线程才能执行此方法
if(Thread.currentThread().getName().equals("A")){
while(true){
}
}
}
public synchronized void testB(){
//只有名为B的线程才能执行此方法
if(Thread.currentThread().getName().equals("B")){
System.out.println("线程B打印此方法");
}
}
}
synchronized底层实现:
加锁的开销大,需要从用户态切换到内核态
同步代码块的底层实现:
执行同步代码块后首先要执行moniterentor指令,退出时执行monitorexit指令。
使用synchronized实现同步,关键点是要获取对象的监视器monitor对象,当线程获取到monitor对象后,才可以执行同步代码块,否则就只能等待。
同一时刻只有一个线程可以获取到该对象的monitor监视器。
通常一个monitorenter指令会同时包含多个monitorexit指令。因为JVM要确保所获取的锁无论在正常执行路径或是异常执行路径都能正确解锁。
同步方法底层实现
当使用synchronized标记方法时,字节码会出现访问标记ACC_SYNCHRONIZED,该标记表示在进入该方法时,JVM需要进行monitorenter操作。在退出该方法时,无论是否正常返回,JVM均需要进行monitorexit操作。
当JVM执行monitorenter时,如果目标对象monitor的计数器为 0,表示此时该对象没有被其他线程所持有。此时JVM会将该锁对象的持有线程设置为当前线程,并且将monitor计数器+1;
在目标锁对象的计数器不为0的情况下,如果锁对象的持有线程是当前线程,,JVM可以将计数器再次+1(可重入锁:当线程拥有当前对象的锁后,可以随便进入该对象的所有同步方法,不用每次都加锁);否则需要等待,知道持有线程释放该锁
当执行monitorexit时,JVM需将锁对象计数器减一,当计数器减为零时,代表该锁已经被释放掉,唤醒所有正在等待的线程。