JavaSE | 多线程
一、实现线程的方式一:继承java.lang.Thread类
1、步骤
(1)编写线程类,继承java.lang.Thread类
(2)重写public void run(){} 在run()的方法体中,编写的代码就是该线程的线程体
(3)创建线程对象:new
(4)启动线程:调用start()
二、实现多线程:实现java.lang.Runnable接口
1、步骤
(1)编写线程类,实现java.lang.Runnable接口
(2)实现public void run(){} 把该线程需要完成的任务,写在run()中
(3)创建线程对象
(4)启动线程:start()
因为Runnable接口和Object中都没有start方法,只有Thread类中有start
所以启动线程必须有Thread的对象。
相当于用Thread对象做为当前线程对象的代理对象,帮我们启动线程。
共享变量--线程的交互
而进程之间的交互特别慢 1套系统即1个进程 rmi --->EJB --> spring
Thread常用方法:
(1)静态方法:currentThread()获取当前线程对象
(2)获取线程名称:getName()
(3)获取线程的优先级:getPriority()
MIN_PRIORITY:最小优先级:1
MAX_PRIORITY:最大优先级:10
NORM_PRIORITY:默认优先级,普通优先级:5
main线程的默认优先级是5,通过主线程创建和启动的线程,默认优先级和它一样
换句话说,谁把你创建和启动的,你的默认优先级就和它一样。当然可以自己修改。
优先级高的,获取CPU调用的概率相对高,但是不是说低没有机会。
优先级必须设置为[1,10]之间,而且必须在启动之前设置。
控制线程的一些方法:
(1)静态方法:sleep(毫秒) 1秒 = 1000毫秒
(2)非静态的方法:
join() 加塞 无限制阻塞下去
join(时间) 只阻塞指定时间
(3)静态方法:yield()
暂停当前线程,让出本次的CPU资源
(4)setDaemon(boolean on):守护线程
守护线程,只为其他线程服务的,如果被守护线程都死亡了,守护线程自动结束了。例如垃圾回收线程
isDaemon():判断某个线程是否是守护线程
(5)停止线程
stop()已过时,现在停止线程,使用一些条件变量来控制线程。
跳出run方法:return、throw
线程安全问题
并发和并行的概念
多线程并发,多个去抢,只有一个执行,单核只有并发
多个软件进程并行,多核
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public class TestThreadSafe { public static void main(String[] args) { final ShareData sd = new ShareData(); Thread t1 = new Thread(new Runnable() { @Override public void run() { sd.username = "alex"; try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t1 = " + sd.username); } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { sd.username = "kris"; try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t2 = " + sd.username); } }); t1.start(); t2.start(); System.out.println("main方法执行完毕。。。"); } } class ShareData{ public String username; }
t1、t2都要sleep 1s,则main线程最先执行; 共享内存(堆、方法区)
一个线程即一个栈空间,堆内存是共享的; 执行方法即压栈--栈帧(方法的所有内容) ;方法执行完就会弹栈
线程安全问题:当多个线程使用同一份“资源”时,其中一个线程对“资源”的修改,影响了其他线程。
多线程在并发执行时,对共享内存中的共享对象中的共享属性进行修改所导致的数据冲突问题;
下次我们如何判断我们的程序是否有线程安全问题?
(1)多个线程; (2)有共享数据; (3)多条语句操作共享数据
多条语句的中间随时可能失去CPU资源,被其他线程操作
如何避免? 加锁; 用同步:
1、同步代码块
synchronized(锁对象){
需要加锁的代码
};
锁对象的选择:
(1)锁对象可以是任意类型的对象; (2)保证多个线程要共用同一个锁对象 ,这个锁对象也称为监视器对象
2、同步方法
【修饰符】synchronized 返回值类型 方法名(【形参列表】)【抛出的异常列表】{
}
锁对象没的选:
(1)静态方法:当前类的Class对象; (2)非静态方法:this(要谨慎)
* 提醒:
(1)锁的代码的范围,太大或太小都不行; 一般考虑一次任务,并且保证所有操作共享数据的代码都锁进去了。
例如,这里的条件判断hasTicket()和买票的buy()代码
(2)一定要确保锁对象是同一个
什么时候释放锁?
(1)把同步代码块或同步方法的代码执行完,自动释放
继承Thread父类
同步代码块
线程开始执行同步代码块之前,必须先获得对同步监视器的锁定,换句话说没有获得对同步监视器的锁定,就不能进入同步代码块的执行,线程就会进入阻塞状态,直到对方释放了对同步监视器对象的锁定。
任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行结束后,该线程自然会释放对同步监视器对象的锁定。
Java程序运行使用任何对象来作为同步监视器对象,只要保证共享资源的这几个线程,锁的是同一个同步监视器对象即可
选择同步共享资源对象作为同步监视器对象
//方法一:同步代码块 public void safe(){ while(ts.hasTicket()){ synchronized (ts) { //synchronized(同步监视器对象){ 需要加锁的代码 }; if(ts.hasTicket()){ String buy = ts.buy(); System.out.println(Thread.currentThread().getName() +"卖了张票" + buy); } } } }
同步方法
与同步代码块对应的,Java的多线程安全支持还提供了同步方法,同步方法就是使用synchronized关键字来修饰某个方法,则该方法称为同步方法。对于同步方法而言,无须显式指定同步监视器,静态方法的同步监视器对象是当前类的Class对象,非静态方法的同步监视器对象是调用当前方法的this对象。
//方法二:同步方法;锁对象是当前类的Class对象Window.class public static synchronized void safe(){ if(ts.hasTicket()){ String buy = ts.buy(); System.out.println(Thread.currentThread().getName() + "卖了张票" + buy); } }
实现Runnable接口的方式
选择this对象作为同步监视器对象
如果线程是继承Thread类实现的,那么把同步监视器对象换成this,那么就没有起到作用,仍然会发生线程安全问题。因为两个线程的this对象是不同的。
但是如果线程是实现Runnable接口实现的,那么如果两个线程共用同一个Runnable接口实现类对象作为target的话,就可以把同步监视器对象换成this。
//选择this对象作为同步监视器对象 public void safe(){ while(true){ synchronized (this) { if(ts.hasTicket()){ String buy = ts.buy(); System.out.println(Thread.currentThread().getName() + "卖了张票" + buy); } } } }
同步方法
//方法二:同步方法;非静态方法的同步监视器对象是调用当前方法的this对象。 public synchronized void safe(){ if(ts.hasTicket()){ String buy = ts.buy(); System.out.println(Thread.currentThread().getName() + "卖了张票" + buy); } }
两种实现线程方式的区别:
1、继承Thread类
(1)启动线程比较简单
(2)可能会遇到单继承的限制
(3)选择锁对象,必须是静态的,或当前类.class
2、实现Runnable接口
(1)启动线程需要借助一个Thread类的对象
(2)没有单继承的限制
(3)选择锁对象,可以使用this,或者别的
生产者与消费者问题
该问题描述了两个(多个)共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”
——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。
与此同时,消费者也在缓冲区消耗这些数据。
该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
问题:
(1)数据的缓冲区是有限的-->需要协作
(2)数据是共享的-->线程安全问题
如何解决?
(1)协作的问题:线程通信 wait()和notify()/notifyAll()
wait()和notify()方法必须由“锁”对象,或监视器对象来调用。
因为多个线程之间通信就可以依据这个“锁”对象,他们直接沟通的桥梁。
IllegalMonitorStateException:非法的监视器对象
什么时候才有监视器对象?在同步代码块或同步方法中,选择的锁对象才是监视器对象
(2)线程安全问题:同步、加锁
当有多个生产者与多个消费者时?
(1)当wait()被唤醒后,要记得重新判断条件,所以这里可以用while,或if..else
(2)notify必须换成notifyAll()
wait()和notify()/notifyAll()在哪里?
在java.lang.Object类中声明的,因为调用它的对象不是线程对象,而是“锁”对象,
因为“锁”对象不知道是什么类型,是任意类型,所以只能声明在Object类中。因为Object中的方法,才能保证所有对象都有。
例如:
快餐店,厨房的厨师与服务员就属于生产者与消费者,他俩之间共同操作工作台上的“菜”,
厨师属于生产者(负责往工作台上放“菜”),服务员属于消费者(从工作台上取走“菜”),
多个生产者消费者,notify( )的是所有同一个锁对象的,等待的线程都有可能被唤醒,因此唤醒的可能是另一个生产者;两个生产者互相唤醒,就会一直生产了;
解决方案是:1)生产者唤醒消费者,消费者唤醒生产者,但notify没有这个功能;
2)每次被唤醒的线程,重新判断条件,不要走下面的,再回去,用while循环;
单例设计模式:(高频面试题)
单例:唯一的对象
* 某个类在整个系统中只有唯一的对象,不会出现第二个对象。
如何实现单例?
(1)构造器私有化:避免随意new对象
(2)在类中创建这个唯一的对象,并且保存:供外界使用
两种形式:
1、饿汉式
无论别人现在是否需要这个对象,我都创建,在初始化这个类时,就创建
2、懒汉式
只有在别人来拿这个对象时(不得不创建时),才会创建
* 单例唯一改变的就是创建对象的位置和获取对象的方式而已,其他的没有变。
* 回忆:枚举,当某个类型的对象是有限个
饿汉式单例模式
class Hungry{ //饿汉模式
// 在类中创建这个唯一的对象,并且保存:供外界使用
public static final Hungry INSTANCE = new Hungry(); //静态的在类初始化时初始化;
//在初始化这个这个类时就创建这个对象,不管别人是否使用;
private Hungry() { //构造器私有化,避免随意私有化对象
}
public void test(){
System.out.println("饿汉式");
}
}
class Hungry2{ //饿汉模式
// 在类中创建这个唯一的对象,并且保存:供外界使用
public static final Hungry2 INSTANCE = new Hungry2();
//在初始化这个这个类时就创建这个对象,不管别人是否使用;
private Hungry2() { //构造器私有化,避免随意私有化对象
}
public void test(){
System.out.println("饿汉式");
}
public static Hungry2 getInstance() {
return INSTANCE;
}
}
enum Hungry3{
INSTANSE;
public void test(){
System.out.println("饿汉式模式");
}
}
懒汉模式
public class TestSingle {
static Lazy l1;
static Lazy l2;
public static void main(String[] args) {
Thread t1 = new Thread(){
public void run(){ //匿名内部类创建的线程对象;
l1 = Lazy.getInstance();
}
};
t1.start();//等它们执行完,再打印它
Thread t2 = new Thread(){
public void run(){
l2 = Lazy.getInstance();
}
};
t2.start();//等它们执行完,再打印它
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(l1); //打印hash码看是否一样;
System.out.println(l2); //它们两个结果不一样,判断出new了两个对象
}
}
class Lazy{
private static Lazy instance;
public Lazy() {
super();
}
public static Lazy getInstance(){ //不能写成 return new Lazy();这样子是调一次new一次;
if(instance == null){ //有可能一个线程刚刚判断为null,CPU被抢走了,另外一个线程也被判为null
//synchronized (Lazy.class) {
// if(Lazy.class == null){ //存在线程安全问题
instance = new Lazy(); //保存到一个变量里边;
}
//}
//}
return instance;
}
}
加锁--->>>
InnerLazy.method(); // 调用外部类的时候就没有创建内部类对象
InnerLazy instance = InnerLazy.Inner.instance;
//只有在别人来拿这个对象时(不得不创建时),才会创建内部类对象
class Lazy{
private static Lazy instance;
public Lazy() {
super();
}
public static Lazy getInstance(){ //不能写成 return new Lazy();这样子是调一次new一次;
if(instance == null){
synchronized (Lazy.class) {
if(Lazy.class == null){ //存在线程安全问题
Lazy instance = new Lazy(); //保存到一个变量里边;
}
}
}
return instance;
}
}
//内部类的方式
class InnerLazy{
private InnerLazy() {
}
public static void method() {
System.out.println("外部类的静态方法");
}
static class Inner{
public static InnerLazy instance = new InnerLazy();
static{
System.out.println("内部类的静态变量");
}
}
}
wait方法和sleep方法的区别?
* (1)wait在Object类中,sleep在Thread类中声明
* (2)wait通过“锁,监视器”对象,sleep通过Thread类名调用
* (3)wait方法使得当前线程进入阻塞状态后,会释放锁;
* sleep方法使得当前线程进入阻塞状态后,仍然持有锁;
yield()、sleep和wait的区别?
* (1)yield()不会阻塞线程,只是暂停线程一次,回到就绪状态
* (2)sleep和wait会阻塞线程
线程池
不要频繁的去创建线程,而是给个固定的,用时拿去用,用完还回来,可以重复利用的效果; 数据库的连接池也是这样;
public class TestThreadPool { public static void main(String[] args) { // 构建线程池 ExecutorService executorService = Executors.newSingleThreadExecutor(); //创建单一线程,只有一个线程去执行 ExecutorService executorService1 = Executors.newFixedThreadPool(5);//可以指定线程的个数 ExecutorService executorService2 = Executors.newCachedThreadPool(); //循环10次,它有可能创建5/6/7/8/9/10个线程都有可能 for (int i = 0; i <= 10; i++){ executorService2.submit(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName()); } }); } } }
Executors.java return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); ThreadPoolExecutor等同于构建了线程池的构建对象;LinkedBlockingQueue阻塞式队列; JDK1.6之后增加两个新队列BlockingQueue阻塞式队列(从队列中去取数据,取不到就一直阻塞直到取到为止;往里边放如果放不进去就一直等待直到放进去)和 Deque双端队列(两头都可以去取)---Kafka底层中用到这个