目录
基础概念:
线程: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"));
}
}