由浅入深——java多线程

 

目录

1、多线程类的两种声明方式

1 继承

2 实现

2、关于容器

3、同步修饰符 synchronized

4、线程命名

5 isALive()方法和getId()方法

6 结束线程的三种方式

1、等待线程的run方法跑完

2、stop()

3、interrupt()

7、暂停和启动

8、出让方法yield() 

9、线程优先级

10 守护线程


1、多线程类的两种声明方式

实现Runable接口和继承Thread类。如果继承Thread类需要复写run方法,如果是实现Runable接口需要实现run方法。

我们后续将这个类称为线程实现类。

继承和实现的差异中有一条,实现类不能自己start,他仅包含一个run方法,纯粹是为了满足Thread需要覆盖run方法的需要,所以如果你在runable中定义了其他方法,是用不了的,只有在继承类中自定义方法,然后继承类自身start才可以使用

1 继承

public class T1 extends Thread{
	public  void run() {
		System.out.println(Thread.currentThread().getName()+"   Thread的子类T1");
	}
}

2 实现

public class T2  implements Runnable{
	public void run() {
		System.out.println(Thread.currentThread().getName()+"   runable");
	}
}
public class MainFunc {
	public static void main(String[] args) {
		// 普通
		Thread thread = new Thread(new Runnable() {	
			public void run() {
				System.out.println(Thread.currentThread().getName()+"   normal");		
			}
		});
		// lambda表达式
		Thread thread2 = new Thread(
			() -> System.out.println(Thread.currentThread().getName()+"   lambda")
		);
		// new Thread()的参数要求是一个runable的实现类,Thread本身就是runbale的实现类所以Thread的子类也可以
		// T1是继承Thread的类
		Thread thread3 = new Thread(new T1());
		// T2是实现接口的类
		Thread thread4 = new Thread(new T2());
		thread.start();
		thread2.start();
		thread3.start();
		thread4.start();
		// Thread的子类本身也可以start,因为父类Thread中有start方法
		new T1().start();
		// Runable只是一个接口并没有start方法和方法体
		// Runnable runnable = ()->System.out.println("runable");
		// runnable.start();
		System.out.println(Thread.currentThread().getName()+"已经执行结束了");
	}
}

运行效果

2、关于容器

上面的代码例子中可以经常看到这种调用模式:

Thread thread = new Thread(Runable target);

我觉得这种处理有点像将一个线程实现类对象交给一个Thread去管理,所以将他称为容器。

每次将对象交给Thread其实是一个线程实现类交给这个容器,然后什么时候执行run方法主线程管不了,所以在代码流程中,主线程这一步做的是 交出管理权,至于后面线程怎么执行,是否成功失败,主线程不管,直接继续执行。但是在主线程中启动的所有线程执行完之前,主线程是不结束的。

如果多个线程实例中管理的线程实现类对象是同一个对象,对象又有线程共享变量,可能就有线程安全问题。如下

定义一个线程实现类,私有成员属性count

public class T1 extends Thread{
	private int count = 5;
	public  void run() {
		System.out.println(Thread.currentThread().getName()+"count值为"+count--);
	}
}

在main方法中定义多个Thread对象,但是操作的对象是同一个,代码如下

public static void main(String[] args) {
		T1 t1 = new T1();
		Thread thread = new Thread(t1);
		Thread thread2 = new Thread(t1);
		Thread thread3 = new Thread(t1);
		thread.start();
		thread2.start();
		thread3.start();
	}

多线程运行具有随机性,所以我们执行了三次,结果如下

原因是因为这里的count--其实背后的字节码有很多步骤,如下

这里我认为线程执行的最小单位就是字节码(不确定),所以在A线程走到读取前,B线程已经对数值做了操作,导致A线程看到的值和他应该看到的不一样。

3、同步修饰符 synchronized

已知锁是加在方法体前面,我认为后面应该还有加载代码块上的方法,根据之前学的jvm,锁的操作应该就是在代码块前后加了指令。

锁做的事情就是当这个线程切换后其他线程一旦想执行这个代码块里的代码,必须要获取到锁才行,但是如果之前拥有锁的线程的代码块没有执行到最后一句的话这个锁是不会被释放的。

如果给代码加上同步修饰符,会显示如下

线程实现类代码如下

class T1 extends Thread{
	private int count = 5;
	public synchronized void run() {
		System.out.println(Thread.currentThread().getName()+"count值为"+count--);
	}
}

运行结果如下(无论执行多少次都一样)

1、锁的地方

锁的地方应该是加锁的最小的地方,如一个方法可能某个地方某个对象可能出现线程安全问题,那么只需要在修改这个对象数据的地方加上锁即可,而不是在整个run上加同步修饰符

如下代码是两个线程操作同一个对象导致数据安全问题的代码:

package synchronizedDemo;

public class MainFunc {
	public static void main(String[] args) {
		ShowObj showObj = new ShowObj();
		showObj.setName("123");
		Trd trd = new Trd(showObj);
		trd.setName("A");
		Trd trd2 = new Trd(showObj);
		trd2.setName("B");
		trd.start();
		trd2.start();
		
		
	}
}


package synchronizedDemo;

class ShowObj {
	private int num;
	private String name;

	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}

	public synchronized void show() {
		System.out.println(name);
	}
};

package synchronizedDemo;

class ShowObj {
	private int num;
	private String name;

	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}

	public synchronized void show() {
		System.out.println(name);
	}
};

 

如果每次操作的不是同一个对象就没问题

如果操作同一个对象,对象的A方法是加锁的,B方法没有加锁,那么只有A方法是线程安全的,因为java指令中的加锁释放锁是固定的指令。只在指令之间的代码有效。

4、线程命名

在线程实现类中使用this.setName();

public class T1 extends Thread{
	private int count = 5;
	public T1(String name) {
		this.setName(name);
	}
	public  void run() {
		System.out.println(Thread.currentThread().getName()+"计算"+count++);
        System.out.println(this.getName()+"我的名字");
	}
}

this.getName是给线程命名的名字,Thread.currendThread().getName()是当前线程的名字。

如果定义了T1的一个实例t1,命名为t1Name,但是将他交给Thread thread = new Thread(t1); thread.start();

那么这里的当前线程名会是Thread-x,而this.getName还是t1自己的名字t1Name

5 isALive()方法和getId()方法

isAlive是判断线程对象是否存活,代码如下

public static void main(String[] args) throws InterruptedException {
		Thread thread = new Thread(new T1());
		thread.start();
		System.out.println("睡觉前"+thread.isAlive());
		Thread.sleep(200);
		System.out.println("一觉醒"+thread.isAlive());
	}

getId()方法用于获取线程唯一ID

6 结束线程的三种方式

1、等待线程的run方法跑完

很常规的方法不解释

2、stop()

调用对象的stop方法即可立即停止,但是已经过期,这个方法不要用。使用方法如下

Thread t1 = new Thread(Runable);

t1.start();

t1.stop();

3、interrupt()

这个方法并不具备停止线程的能力,只是将线程是否中断的状态设置为ture,线程可以在内部判断当前状态并作出不同处理。

我认为这是一种线程之间交流的方式。

可以配合return或者抛异常实现停止线程

7、暂停和启动

暂停和启动方法分别是suspend和resume,用的不好经常可能导致线程无法继续下去,如图,线程的功能是获取currentTimeMills,在暂停后两次之间时间没变没有计时,后面继续后又计时了

上面是暂停写在线程外的情况,如果将暂停写在线程实现类内部的话,又没有调用继续方法的代码,可能这个线程就会一直暂停,如果这个暂停方法是synchronized代码体内的话,那么其他线程将永远获取不到这段代码的锁。

这里另一个线程B永远执行不了,end也打印不出来

println方法也是加锁的,如下代码,最后一行end永远打印不出来,因为线程暂停了,锁在println中一直没有释放出来

8、出让方法yield() 


出让方法是指执行到当前线程时把时间片让出去给其他线程,但是可能刚出让下一次又抢到时间片了所以时间不一定,下面代码距离了一个线程中加上yield和不加yield执行时间的差别。

public void run() {
		long start = System.currentTimeMillis();
		// 加yield是3747毫秒
		// 不加是3毫秒
		for(int i=0; i<50000000;i++) {
			//yield();
		}	
		long end = System.currentTimeMillis();
		System.out.println(end-start);
	}

yield 和sleep的区别

yield只让给同级别或者高级别的线程,sleep不分等级

sleep更符合cpu调度,移植性更好

yield是再次回到就绪装填,sleep是回到阻塞状态,yield可能下一秒就执行了,sleep一定时间内一定不执行

9、线程优先级

线程优先级设置方法setPriority,取值范围1-10,越大优先级越高,差值越大两个线程的优先级别越明显。如下代码将两个线程分别设置为1和10,并且跑了十次,结果显示线程为10的都先跑完了。

public class MainFunc {
	public static void main(String[] args) throws Exception {
		int priB = 10;
		System.out.println("本次b的优先级是"+priB);
		for(int i=0;i<10;i++) {
			pri(i,priB);
		}
	}
	
	public static void pri(int num,int priB) throws Exception{
		Thread thread1 = new Thread(()->{
			for(int i=0; i<100000; i++) {
			}
			System.out.println("a"+num);
		} );
		Thread thread2 = new Thread(()->{
			for(int i=0; i<100000; i++) {	
			}
			System.out.println("b"+num);
		} );
		thread1.setPriority(1);
		thread2.setPriority(priB);
		thread1.start();
		thread2.start();
	}
}

如下结果分别是b的优先级为10,为5,为3和为1的截图。

如果将b的优先级设置为3,则A偶尔也能抢到,如果设置为2或1,顺序就比较随机。

如果在A的run方法中去start了B线程,那么B线程和A的优先级是一样的

10 守护线程

守护线程是保护进程中所有非守护线程的线程,如果进程中不存在非守护线程,则守护线程销毁。典型的守护线程是垃圾回收线程。

守护线程的方法是setDaemon(true); daemon是守护神的意思。

代码如下:

package daemon;

public class MainFunc {
	public static void main(String[] args) throws Exception{
		Thread thread = new Thread(()->{
			for(int i=0;i<10;i++) {
				System.out.println(i);
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}) ;
		thread.setDaemon(true);
		thread.start();
		Thread.sleep(5000);
		System.out.println("结束");
	}
}

打印如下

在main主线程结束的时候,所有非守护线程就都执行完了,此时守护线程也没有继续执行下去。

猜你喜欢

转载自blog.csdn.net/a397525088/article/details/82156037