Java基础(十二)——多线程

进程与线程

进程:每个程序运行都是一个进程,是计算机分配资源的单位
线程:是CPU的最小执行单元,一个进程至少有1个线程,可以有多个线程
	进程的功能对应CPU的独立执行路径就是线程
	线程依赖进程而存在的

多线程

对于单核处理器,同一时间只能处理一个线程,只是处理器切换线程速度太快,
	让我们以为是多个程序一起运行
对于多核处理器,同一时间能处理多个线程
多线程的目的仅仅是提高单个CPU的使用率,而不是提高程序的使用效率
多进程是CPU在程序间做着高效的切换让我们觉得是同时进行的

程序运行原理
	a.分时调度
		所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间
	b.抢占式调度
		优先让优先级高的线程使用CPU,如果优先级相同随机选择一个
		java使用的是抢占式调度
		线程优先级默认是5
		最高是10,最小是1;
	多线程的用途,当一个方法运行时间过长时,此方法不结束其他方法不能继续往下运行
	此时可以利用多线程在这个方法之外再开一道线程,与之并列执行

Thread

描述线程的类
继承了Object实现Runnable接口 
线程依赖进程而存在,进程由系统创建,Java不能直接调用系统功能的
所以Java提供了java.lang.Thread来描述线程
创建新的执行线程的方法
1.继承Thread重写run方法
2.实现Runnable接口,重写run方法(更加常用)
3.实现实现Callable接口,重写call方法,这种方式有返回值
常用方法:
start():启动线程,JVM虚拟机调用该线程的run方法
run():执行方法,没有启动线程的功能
static void sleep(long l):当前线程休眠多少毫秒
						sleep方法会让线程进入阻塞状态
final void join():等待该线程终止,即该线程执行完毕后才能执行其他线程
static void yieid():暂停当前正在执行的线程对象,
						使当前线程重新回到就绪状态
						yield方法并不会让线程进入阻塞状态
final void setDaemon(boolean b):为true,将线程设置为守护线程		
					随着主线程代码的结束而结束整个守护线程
					在线程启动之前调用
void interrupt():线程在调用object的wait方法或
				Thread的join,sleep方法受阻,中断线程,
				并抛出一个InterruptedExpction,
				然后继续向下执行
										
final void stop():该方法具有不安全性,不建议使用,
			已过时,不会抛出InterruptedExpction异常

多线程方式一

继承Thread重写run方法
public class MyThread extends Thread {
	@Override
	public void run() {
		for (int i = 0; i < 50; i++) {
			System.out.println("新线程" + i);
		}
	}
}

public static void main(String[] args) {
	MyThread thread = new MyThread();
	thread.start();// 开启新线程
	for (int i = 0; i < 50; i++) {
		Thread.sleep(10);//静态方法直接调用,使当前线程休眠10毫秒
		System.out.println("main线程" + i);
	}
}

//main线程0
//新线程0
//main线程1
//新线程1
//新线程2
//新线程3
//新线程4
//main线程2

重写run方法?为什么?

一个类中不是所有代码都需要开启新的线程执行
所以用run方法来包含哪些需要被新线程执行的代码
多线程的执行需要调用Thread类的start方法

start()与run()的区别:

run():仅仅是封装了能被线程执行的代码,直接调用就是普通方法,
start():首先启动了线程,然后由JVM虚拟机调用了该线程的run方法
注意:同一个线程调用2次start方法,会报错,非法的线程状态异常,
	一个线程不能启动2次

内存中的线程

栈内存中,每个线程在执行时都会在内存中开辟一片属于自己的栈空间,
独自进行压栈和弹栈
多个线程间共享代码,数据,文件,而寄存器与栈是独立的

设置线程名称

	设置线程名称方式一
	MyThread thread = new MyThread("new");
	需要添加有参构造器
	设置线程名称方式二
	thread.setName("lisi");
	//当前线程.getName()方式一
	Thread的子类中直接getName()
	//获取当前线程对象.getName()方式二
	外部类:子类对象.getName()
	Thread.currentThread().getName();获取当前线程名称
public void run() {
		for (int i = 0; i < 50; i++) {
			System.out.println(getName() + i);
		}
	}

MyThread thread = new MyThread("new");
// thread.setName("lisi");
thread.start();// 开启新线程

多线程方式二

实现Runnable接口重写run方法
可以避免单继承的局限性,所以较为常用
// 实现接口的多线程方式的匿名内部类
Runnable r = new Runnable() {
	public void run() {
		System.out.println("Runable");
	}
};
// 调用
new Thread(r).start();

//简单写法
new Thread(new Runnable() {
		public void run() {
			System.out.println("Runable");
		}
	}).start();

// 继承Thread的多线程方式的匿名内部类
new Thread() {
	public void run() {
			System.out.println("new");
		};
	}.start();

线程状态

Thread内部枚举类Thread.state用于描述线程状态	
新建:创建线程对象 —— new ...
阻塞:有执行资格,没有执行权
运行:有执行资格,有执行权——start()
休眠:由于一些操作使得线程处于该状态,没有执行资格,没有执行权,
		休眠时间到了会自动激活,激活后处于运行/阻塞状态;——sleep()
等待:Object类的wait()可以将当前对象的线程置于无限期等待状态
		无法自动激活,需要Object类的notify()来唤醒这个线程处于运行/阻塞状态
死亡:线程对象变成垃圾,等待被回收 ——run()过后或stop

sleep方法和wait方法的区别?

sleep 是线程类(Thread)的方法,导致此线程进入阻塞状态,
	将CPU执行机会给其他线程,但是监控状态依然保持,
	到时后会自动恢复。调用sleep 不会释放对象锁。
wait 是Object 类的方法,对此对象调用wait 方法导致本线程放弃对象锁,
	进入等待此对象的等待锁定池,只有针对此对象发出notify 方法(或notifyAll)后
	本线程才进入对象锁定池准备获得对象锁进入运行状态。
1、这两个方法来自不同的类分别是Thread和Object
2、最主要是sleep方法没有释放锁,而wait方法释放了锁,
	使得其他线程可以使用同步控制块或者方法。
3、wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,
	而sleep可以在任何地方使用(使用范围)
synchronized(x){
  x.notify()
  //或者wait()
  }
4、sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常
注意sleep()方法是一个静态方法,也就是说他只对当前对象有效
例如t是一个线程,通过指定t.sleep()让t对象进入sleep,这样的做法是错误的,它只会是使当前线程被sleep 而不是t线程

为什么wait(),notify()方法要和synchronized一起使用?

因为wait()方法是通知当前线程等待并释放对象锁,
notify()方法是通知等待此对象锁的线程重新获得对象锁,
然而,如果没有获得对象锁,wait方法和notify方法都是没有意义的,
即必须先获得对象锁,才能对对象锁进行操作;
于是,才必须把notify和wait方法写到synchronized方法或
是synchronized代码块中了

线程池

线程是一种资源
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任
务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁
创建线程和销毁线程需要时间
在Java中可以通过线程池来达到线程的复用效果,在线程池中事先创建
多个线程,有线程调用就调用线程池中的线程,用完就放入线程池中

如果运行的线程数大于线程池中的线程数,多余线程处于阻塞状态,
等待其它程序执行完任务后释放线程

优点是节省系统资源
缺点是不好设置初始的线程数

Executors

java.util.concurrent.Executors创建线程池容器的类
方法全是静态的
常用方法:
a.ExecutorService newFixedThreadPool(int x):创建一个数量固定的可复用的线程池
	返回ExecutorService接口的实现类ThreadPoolExecutor(线程池对象)
	
ExecutorService接口的submit/shutdown方法
b.Future<?> submit(Runnable task):获取线程池中某一个线程对象并执行
		传递一个Runnable接口实现类
	Future接口是用来记录线程任务执行完毕后产生的结果
c.shutdown()销毁此线程池
//实现Callable 接口重写run方法的线程类
public class ThreadPoolCallable implements Callable<String>{
	public String call(){
		return "abc";
	}
}
public class ThreadPoolDemo1 {
	public static void main(String[] args)throws Exception {
		//创建并初始化线程池对象
		ExecutorService es = Executors.newFixedThreadPool(2);
		//提交线程任务的方法submit方法返回 Future接口的实现类
		//调用线程并执行获取返回值放入Future接口中
		Future<String> f = es.submit(new ThreadPoolCallable());
		String s = f.get();//获取返回值
		System.out.println(s);
	}
}

线程安全

当一个线程去获取共享数据int i = 100并执行-1操作时,
另一个线程也去获取这个共享数据执行-1操作,
第一个线程获取到数据并执行-1操作后,未返回覆盖共享数据时,
第二个数据去获取共享数据int i = 100,这时第一个线程将修改的
数据返回并覆盖,int i = 99
第二个线程修改后也返回, int i = 99
执行两个-1操作后,共享数据的值只减少了一次,
这就是多线程下的数据安全问题

线程安全问题的产生原因:
1.要有多线程环境
2.要有共享数据
3.要有操作共享数据的行为
针对产生问题的原因,进行解决.只能在数据操作上进行安全保障
进行数据的同步,同一时间只允许一个线程操作共享数据

线程安全的方式

方式一.
	同步代码块:
	synchronized(对象锁){需要同步的代码}	
	同步可以解决安全问题的根本原因在那个对象锁上,
	该对象如同锁的功能,这个对象任意,但必须为同一把锁
	锁对象默认为this
	同步的好处:解决多线程的安全问题
	同步的缺点:当线程相当多的时候,每个线程都会去判断同步上的锁,
	这是很耗费资源,无形中降低程序的运行效率

静态同步方法的锁对象不是this,因为静态随类的加载而加载,
			这时候没有this,
静态同步方法的锁对象是当前类的class文件对象(类的字节码文件对象),
			即xx.class
package threads;
//经典售票案例
public class SellTicket implements Runnable {
	private int num = 100;

	@Override
	public void run() {

		while (true) {
			synchronized (this) {
				if (num > 0) {
					try {
						Thread.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println("由" + Thread.currentThread().getName() + "号售票员卖第" + num-- + "张票");
				} else {
					return;
				}
			}

		}
	}

}

main():
		SellTicket s = new SellTicket();
		//保证对象锁是同一个对象
		Thread t1 = new Thread(s, "窗口1");
		Thread t2 = new Thread(s, "窗口2");
		Thread t3 = new Thread(s, "窗口3");
		t1.start();
		t2.start();
		t3.start();
关于集合的线程安全:
Vector是线程安全的,我们一般不用它
因为Collections有提供线程安全的方法;
如:List是非线程安全的
List<String > list = 
Collections.synchronizedList(new ArrayList<String>)即可

方式二:同步方法
public synchronized void safe() {

}
方式三:Lock接口
方法:
void lock();获取锁
void unlock():释放锁

实现类:ReentrantLock实现了Lock接口和序列化接口

用法:在需要同步的类中定义Lock接口
private Lock lc = new ReentrantLock();
在需要同步代码的地方
try{
	lc.lock()
	同步代码
}finally{
	lc.unlock();
}
数据同步的弊端:
1.效率低
2.如果出现同步嵌套,就容易产生死锁问题
同步嵌套:你用我的锁,我用你的锁
死锁问题:两种或以上的线程在执行过程中,
因为争夺资源出现一种互相等待的现象

定时器

Timer:一种工具,线程用其安排任务一次或定期重复执行
public Timer();定时
public void  schedule(TimerTask task ,Long l),多少毫秒后执行
public void  schedule(TimerTask task ,Long log , Long  o)  :log毫秒后执行第一次,间隔o毫秒后循环执行
cancel()关闭定时器
TimerTask抽象类 :任务
public abstract void run();抽象方法
public boolean cancel();
Class MyBoom extends TimerTask{
private Timer t;
public MyBoom (){}
public MyBoom (Timer t){ this.t = t}
public  void run(){
system.out.print("爆炸了");
t.cancel();关闭定时器
}
}
测试类:
..main(...){
创建定时器对象
Timer ti = new Timer();
3秒后执行爆炸任务
ti.schedule(new MyBoom(ti) ,3000);
}

定时到未来某一刻的任务:
Timer ti = new Timer();
String s = "2099-12-12 15:00:00";
SimpleDateFormat sdf = new  SimpleDateFormat ("yyyy-MM-dd HH:mm:ss");
Date d = sdf.parse(s);
ti.schedule(new MyBoom(ti) ,d);
发布了30 篇原创文章 · 获赞 0 · 访问量 355

猜你喜欢

转载自blog.csdn.net/qq_31241107/article/details/104280472