JAVA多线程:Thread类与Runnable类

目录

基础概念:

JAVA中的多线程:

Thread类:

Runnable接口:

线程池:

线程的安全问题:

锁与死锁:

定时器与定时任务:


基础概念:

线程:Thread,进程中的一个执行路径/执行单元。

进程:process,一个程序或软件的运行。本质是程序在内存中分配的空间,但一个软件可能有多个进程。

eg:进程中至少有一个线程,同时可以有多个线程,即多线程程序。

并行:真正意义的同时执行(多核CPU同时工作)

并发:多个程序单独执行(CPU轮换执行),执行速度很快,近乎同时工作,但某一时刻只有一个程序在工作。

eg:四核八线程CPU:正常四核工作,超频状态下会模拟八核工作,即八线程。

单线程程序:控制面板中的删除程序,需要排队。用户体验差

多线程的优点:提高了用户体验,以及CPU的利用率。


JAVA中的多线程:

线程 是程序中的执行线程。Java 虚拟机允许应用程序并发地运行多个执行线程。

java中实现多线程的方式有三种:1.继承Thread类;2.实现Runnable接口(Thread类实现了Runnable接口);3.线程池

1.将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法 (run 方法本质是线程任务。可以不重写,但无任何意义)。接下来可以分配并启动该子类的实例,开启新线程的命令是:对象名.start()。注意,直接调用 对象名.run()方法不是开启线程的命令。

2.创建线程的另一种方法是声明实现 Runnable 接口的类。此时必须实现接口中为抽象方法run()。开启新线程的命令是:对象名.start()。但必须依赖Thread类,因为对象时Thread类型的。

注:只有线程对象才能调用start方法,而只有Thread以及Thread子类的对象才是线程对象。

两种方式的区别:

实现 Runnable 接口的类的方式解决了单继承的局限性(一个类只能继承一个父类)
实现 Runnable 接口的类的方式将线程对象与线程任务分离开,更加符合面向对象的思想:高内聚低耦合


Thread类:

子类继承Thread类后,子类实例化的同时会调用父类Thread中的构造方法,从而产生一个Thread类型的线程对象,再由线程对象调用 start 方法开启新线程。这种形式下,线程对象与线程任务是在一起的

 * 1.程序执行时会开启主线程main,执行main方法。即一个进程至少有一个线程在工作
 * 2.执行到方法时,直到方法执行完毕才会进 入下一阶段。即单线程工作
 * 3.如果类对象继承了Thread类,那么main方法中会自动给每个对象赋线程名
 * 4.当main中最后一个是开启新线程时,新线程开启完毕,主线程就会结束,其它线程继续执行
 * 5.Thread类中的getName方法是用final修饰,不能重写。
 * 6.新线程开启后,只是拥有了执行权限,具体是否执行是CPU决定的。

public class ThreadDemo1{
	public static void main(String[] args) {
		Demo d1 = new Demo("唐三");  //实例化时,会默认调用父类中的构造方法,Thread类中的构造方法可以创建一个新的Thread对象
		Demo d2 = new Demo("小舞"); 
		System.out.println(Thread.currentThread().getName());
		d1.setName("唐三Thread");
		d2.setName("小舞Thread");
		System.out.println(d1.getName());
		System.out.println(d2.getName());
		d2.start();	 //新线程
		d1.run();   //方法:有主线程main调用。结束之后往下继续进行		
	}
}
class Demo extends Thread {
	String name;
	public Demo(String name) {
		this.name = name;
	}
	public void run() {      //线程任务
		for(int i=0;i<5;i++) {
			System.out.println(Thread.currentThread().getName() + "--" + name + "");
		}		
	}
}

线程阻塞:

public static void sleep(long millis):静态方法,进入阻塞状态,时间结束后进入可执行

public class ThreadDemo2 {
	public static void main(String[] args) {
		Demo2 d1 = new Demo2();
		Demo2 d2 = new Demo2();
		d1.start();
		d2.start();
		/**
		 * 静态方法使用对象调用,本质上还是用类名调用,
		 * 不是让d1线程睡了3s,而是让主线程睡了3s,
		 * 因为它定义在方法中。
		 */
		try {
			d1.sleep(3000);   //本质还是:Thread.sleep();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}
class Demo2 extends Thread{ 
	public void run() {   //run方法中调用sleep方法,处理异常的方式只有try...catch。是因为父类Thread中run方法没有使用throws处理异常
		try { 
			Thread.sleep(3000);   //Thread.sleep()定义在线程任务中,则是让线程任务睡眠3s
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		for(int i=0;i<10;i++) {
			System.out.println(Thread.currentThread().getName());
		}
	}
}

public final void join():成员方法,被谁调用,让哪个线程先执行,执行完毕后,再执行所在的线程

注:只能控制两个线程:一个是调用的线程,一个是所在的线程

public static void yield():让步,让其他线程先执行,不一定生效,因为是CPU决定的。在高并发环境下有效。

public class ThreadDemo3 {
	public static void main(String[] args) {
		Sum s = new Sum();
		s.start();  //开启新线程
		try {
			s.join();  //被那个对象调用,就让那个线程先执行完,执行完毕后,再执行他所在的线程
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(Sum.sum);   //新线程启动之后,主线程会执行下一语句。但这个时候sum可能还没算完。
	}
}
class Sum extends Thread{
	static int sum = 0;
	public void run() {
		for(int i=0;i<=100000;i++) { 
			sum += i;
			Thread.yield();  //让步给另一个线程,但不一定奏效,执行权在CPU手里。但在高并发环境下可以使线程尽可能均匀执行
		}
	}
}

中断阻塞线程:

public final void stop()停止一个线程,已过时

public void interrupt():打断线程的阻塞状态,进入可执行状态,会抛出异常:java.lang.InterruptedException: sleep interrupted

1.中断普通线程的睡眠状态:

public class ThreadDemo4 {
	public static void main(String[] args) {
		Demo3 d = new Demo3();
		d.start();  //新线程
		d.interrupt(); //中断d对象线程的阻塞状态
		System.out.println("over");
	}
}
class Demo3 extends Thread{
	public void run() {
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		for(int i=0;i<30;i++) {
			System.out.println(Thread.currentThread().getName()+"======"+i);
		}
	}
}

2.中断主线程的睡眠状态:

创建一个子线程,在子线程的run方法中使用Thread.currentThread(),表示当前线程,调用interrupt方法唤醒主线程。

注:由于主线程是顺序执行的,当开启新线程(包含interrupt方法)后,主线程可能还没有进入睡眠状态,此时可以在线程任务中添加10ms的延迟,确保执行interrupt方法时,主线程已进入睡眠状态。

public class ThreadDemo6 {
	public static void main(String[] args) {
		Demo5 d = new Demo5(Thread.currentThread()); //
		d.start();
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("over");
	}
}
class Demo5 extends Thread{
	Thread th;
	public Demo5(Thread th) { //传入一个线程类对象
		this.th = th;
	}
	public void run() {
		try {
			Thread.sleep(10);   //保证主线程已经进入了阻塞状态
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		th.interrupt();  //中断阻塞状态
	}
}

Runnable接口:

 步骤:
 * 1.实现Runnable接口
 * 2.重写run方法
 * 3.创建Runnable子类对象:是线程任务(重写的run方法)对象,不是线程对象
 * 4.创建Thread对象,把Runnable子类对象传到构造方法中
 * 5.使用Thread对象调用start方法

public class RunnableDemo1 {
	/**
	 * 练习:一个线程打印大写字母,一个打印小写字母
	 * 分析:使用两个类去实现Runnable方法
	 * @param args
	 */
	public static void main(String[] args) {
		Demo05 d = new Demo05();   //线程任务
		Thread th = new Thread(d);  //线程对象
		th.start();
		new Thread(new Demo06()).start();  //使用匿名内部类的方式
	}
}
class Demo05 implements Runnable{
	@Override
	public void run() {   //线程任务
		for(char i='a';i<='z';i++) {
			System.out.print(i + ",");
			Thread.yield();  //在高并发环境下,该方法可以提高多个线程的均匀执行的概率
		}
	}	
}
class Demo06 implements Runnable{
	@Override
	public void run() {   //线程任务
		for(char i='A';i<='Z';i++) {
			System.out.print(i + ",");
			Thread.yield();
		}
	}	
}

线程池:

线程池也可以创建新线程,执行线程任务。那为什么要使用线程池呢?程序使用start方法启动一个新线程的成本是比较高的,因为涉及到与操作系统的交互。而使用线程池可以很好的提高性能,即线程池中包含大量处于空闲状态的线程,每当有新的线程对象进来的时候就会分线程给它,线程任务结束后,他还会回到线程池,等待下一个对象调用。尤其程序中要创建大量生存期很短的线程时,更需要使用线程池。JDK5以后java.util.concurrent包新增了Executors工厂类来产生线程池。

1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务;

2.可以根据系统的承受能力,调整线程池中工作线程的数目,.....

public static ExecutorService newCachedThreadPool() ---------------- 创建一个具有缓存功能的线程池,每次创建会活跃60s。如果没有线程可用,会不断创建新的线程(无上限,最大int类型最大值21亿)

public static ExecutorService newFixedThreadPool(int nThreads) --------  创建一个可重用的,具有固定线程数的线程池

public static ExecutorService newSingleThreadExecutor() -----创建一个只有单线程的线程池,相当于上个方法的参数是1 

这些方法的返回值是ExecutorService对象,该对象表示一个线程池。

public void shutdown():线程池不会自动停止,可以使用shutdown方法停止

public class ThreadPoolDemo01 {
	public static void main(String[] args) {
		//方式一:匿名内部类:本质是子类的对象,父类引用指向子类对象
		Runnable r = new Runnable() {  
			@Override
			public void run() {
				try {
					Thread.sleep(300);   //设置睡眠时间是为了显示出多线程效果
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				for(int i=0;i<10;i++) {
					System.out.println(Thread.currentThread().getName());
				}
			}		
		};
		//方式二:使用lambda表达式代替匿名内部类
		Runnable r1 = ()->{    //()中传的是方法run()中的参数
			for(int i=0;i<10;i++) {
				System.out.println(Thread.currentThread().getName());
			}
		};
		//获取线程池
		ExecutorService pool = Executors.newCachedThreadPool();  
		//使用线程池执行线程任务
		pool.execute(r);  //同一个线程池,不同的线程对象:pool-1-thread-2
		pool.execute(r); //同一个线程池,不同的线程对象:pool-1-thread-1
		pool.shutdown();  //线程池不会自动停止,可以使用shutdown方法停止
	}
}

线程的安全问题:

Java 虚拟机允许应用程序并发地运行多个执行线程,并不是真正意义上的同时执行。多线程在某一时刻是单线程的。在执行线程任务时,由于延迟导致单个线程还未完全执行完run方法时,CPU就将资源调配给了另外一个线程,在结果上看就是多个线程同时执行了线程任务。导致线程不安全。

在学习单例模式中的懒汉模式以及学习StringBuilder类时,都提到这些是线程不安全的,那具体什么是线程不安全,我们以一个案例来看一下:

火车站卖票:在卖票的过程中,出现了多个窗口买同一张票或卖出负数票的情况

//线程不安全情况代码
public class ThreadSaftyDemo {
	public static void main(String[] args) {
		Tickets t = new Tickets();  //线程任务
		Thread th1 = new Thread(t);
		Thread th2 = new Thread(t);
		Thread th3 = new Thread(t);
		th1.start();
		th2.start();
		th3.start();
	}
}
class Tickets implements Runnable{
	static int tickets = 100;  //共享数据
	@Override
	public void run() {
		while(true) {
			if(tickets>0) {
				try {
					Thread.sleep(600);  //设置睡眠原因:增大出现多线程安全问题的可能性
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName()+"正在出票第:"+tickets--+"票");
			}else {
				break;
			}
		}	
	}
}

 * 原因分析:
 * 1.具备多线程的环境
 * 2.操作共享数据
 * 3.操作共享数据的代码有多个
 * 解决方法:加同步锁! 每次只让一个线程进行操作,操作完毕后,再让其它线程进来

锁与死锁:

 加锁:同步代码块、同步方法、Lock三种方法。
 1.同步代码块 ---------- 使用最多的一种方式,比较灵活。
 *         synchronized ( 锁对象 ) {
 *             容易出现线程安全问题的代码;
 *         }

 *         注:锁对象可以是任意对象。但必须保证在多线程环境下是同一个对象,具体看对象被new了几次
 * 锁对象:
 * 1).字符串:字符串在常量池中,唯一存在
 * 2).类名.class 或者  new一个对象然后  对象名.getClass():字节码对象
 * 3).使用静态的Object对象:静态对象唯一存在
 * 4).在main方法中声明一个Object对象,之后在继承了Thread的类中添加参数为Object对象的构造方法

class Tickets1 implements Runnable{
	static int tickets = 100;
	@Override
	public void run() {
		while(true) {
			synchronized("燃") {
				if(tickets>0) {
					try {
						Thread.sleep(600);  //设置睡眠原因:增大出现多线程安全问题的可能性
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName()+"正在出票第:"+tickets--+"票");
				}else {
					break;
				}
			}	
		}	
	}
}

  2.同步方法(锁的范围过大,变成了单线程。只是不适用于卖票这个案例。比如懒汉式模式)
 *         synchronized放到方法的修饰符上:锁住的是整个方法,当一个线程执行完整个方法之后,其他线程再执行(单线程)
 * 
 *         成员方法:默认对象  this(当前对象)
 *         静态方法:默认对象  类名.class  字节码对象

//懒汉式
public class Singleton2 {
	private static Singleton2 s;// null
	private Singleton2() {
	}
	public synchronized static Singleton2 getInstance() {
		if(s == null) {
			s = new Singleton2();
		}
		return s;
	}
}

 3.Lock:显式锁(since JDK5) --- 接口
          加锁与解锁需要显示的表示出来 :lock():加锁   unlock():解锁
注:一定要保证unlock()代码一定会执行到,否则锁对象释放不了,会导致未解锁,程序陷入死循环,因此,unlock一般放在finally语句中执行(try...finally)

class Tickets4 implements Runnable{
	static int tickets = 100;
	Object o = new Object();
	Lock lock = new ReentrantLock();  //Lock为接口,使用子类实例化对象
	
	@Override
	public void run() {//this
		while(true) {
			try {
				lock.lock();
				if(tickets>0) {
					try {
						Thread.sleep(30);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().
							getName()+"正在出售第:"+ tickets-- +"张票");
				}else {
					break;
				}
			}finally {
				lock.unlock();
			}
		}
	}
}

死锁:

是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待现象

弊端:效率低;如果出现了同步嵌套,就容易产生死锁问题。 --------------------------------------- 尽量避免锁的嵌套使用

public class DiedLock {
	public static void main(String[] args) {
		new MyThread(true).start(); //拿到锁对象A,需要锁对象B
		new MyThread(false).start(); //拿到锁对象B,需要锁对象A
	}
}
class Lock {
	public static Object lockA = new Object();
	public static Object lockB = new Object();
}
class MyThread extends Thread {
	private boolean flag;
	public MyThread(boolean flag) {
		this.flag = flag;
	}
	@Override
	public void run() {
		while (true) {
			if (flag) {
				synchronized (Lock.lockA) {
					System.out.println("lockA");
					synchronized (Lock.lockB) {
						System.out.println("lockB");
					}
				}
			} else {
				synchronized (Lock.lockB) {
					System.out.println("lockB");
					synchronized (Lock.lockA) {
						System.out.println("lockA");
					}
				}
			}
		}
	}
}

Wait,notify,notifyAll这三个方法都是Object中的方法,并且这三个方法必须放在同步代码块或同步方法中。三种方法之所以放入了Object类中,是因为这三种方法必须使用锁对象来执行,而锁对象可以是任意的对象。同时三种方法必须使用相同的锁对象,这样可以唤醒等待池中同一个已阻塞的线程,唤醒之后线程会进入锁池中,进而得到了进入同步代码块或同步方法的权利,但能否进入取决于是否拿到锁对象。

void wait():线程进入等待状态,同时释放锁对象,允许其他线程进入中断了当前线程。会抛出此InterruptedException

void wait(long timeout):类似于Thread类中的sleep方法

void notify():随机唤醒一个等待的线程

void notifyAll():唤醒所有等待的线程

public class WaitDemo {
	public static void main(String[] args) {
		Object o = new Object();
		Demo6 d = new Demo6(o);
		d.start();
		Demo7 d2 = new Demo7(o);
		d2.start();
	}
}
class Demo6 extends Thread{
	Object o;
	public Demo6(Object o) {
		this.o = o;
	}
	public void run() {
		synchronized (o) {
			System.out.println(Thread.currentThread().getName()+":wait--start--");
			try {
				o.wait(); //会释放锁对象,此时允许其他线程进入,但必须取得锁对象
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+":wait--end--");
		}
	}
}
class Demo7 extends Thread{
	Object o;
	public Demo7(Object o) {
		this.o = o;
	}
	public void run() {
		synchronized (o) {
			System.out.println(Thread.currentThread().getName()+":notify--start--");
			o.notify();  //随机唤醒一个wait的等待线程,同时必须使用同一个锁对象唤醒
			System.out.println(Thread.currentThread().getName()+":notify--end--");
		}
	}
}
===========================
Thread-0:wait--start--
Thread-1:notify--start--
Thread-1:notify--end--
Thread-0:wait--end--

定时器与定时任务:

实现定时器依赖于Timer与TimerTask,TimerTask是抽象类。TimerTask只有一个抽象方法run(),TimerTask抽象类实现了 Runnable接口,重写的run方法是使用多线程来执行的,即和主线程main不在一个地方执行;同时定时任务只能执行一次。

void schedule(TimerTask task,long delay):延迟多少毫秒执行任务
void schedule(TimerTask task,Date delay):指定时间来执行定时任务:闹钟,如果时间过期了会立即执行
void schedule(TimerTask task,long delay,long period):delay指多久之后执行,仅一次。period为任务执行间隔周期
void cancel():结束定时器任务,一般放在重写的run()方法中。

public class TimerDemo {
	public static void main(String[] args) throws ParseException {
		//1.创建Timer对象
		Timer t1 = new Timer();
		//2.调度定时任务:使用匿名内部类 -- lambda表达式只适用于接口的改写,不适用于抽象类
		t1.schedule(new TimerTask() {  //使用一个多线程来执行的,和主线程不是在一个地方启动的
			@Override
			public void run() {
				System.out.println("爱你一万年!");	
				t1.cancel();
			}
		}, 1000);
		
		Timer t2 = new Timer();
		t2.schedule(new TimerTask() {
			@Override
			public void run() {
				System.out.println("清风不许笑");
				//定义一个秒表
				System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));		
			}			
		}, 3000, 1000);
		
		//自定义闹钟
		Timer t3 = new Timer();
		t3.schedule(new TimerTask() {
			@Override
			public void run() {
				System.out.println("该起床了!");	
			}	
		}, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2018-12-23 11:37:30"));
	}
}

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值