多线程概述
1.什么是多线程?
在了解线程之前,我们先来了解一下什么是进程。在window系统中我们知道有任务管理器,在任务管理器可以操作系统中的进程。
那什么是进程呢?
进程:就是正在执行的程序,且每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者
叫做一个执行单元。
那这跟线程有什么关系呢?
线程:就是进程中一个独立的控制单,线程控制着进程的执行。
注意:
1)在一个进程中线程却可以有多个,这也是Java支持多线程技术的原因。
换句话说就是一个进程可以被多个控制单元(线程)操作。
2)一个进程至少有1个线程在控制。
3)java虚拟机启动时至少两个线程:一个是main主线程,一个是java.exe。
2.线程的生命周期
我们都知道程序运行都是有生命周期的,线程也不例外。
线程的生命周期分一下几种状态:被创建初始化状态---运行状态--阻塞状态--冻结状态(休眠态和等待态)---消亡状态
下面用图来表示一下这几种状态是如何转换的:
多线程的创建
在代码中如定义多线程?
通过查阅Java的API我们发现:
在Java中有两种定义多线程的方式,一个是继承Thread类一个是实现Runnable接口。
1.继承Thread类
步骤:
a.定义一个类继承Thread类
b.复写Thread类中的run方法,将自定义的代码存放到run方法中。
c.调用线程中的start方法,创建线程并执行run方法中的代码。
代码功能实现:
1)在代码的运行时我们会发现
线程的名字被我们打印出来了,这是因为我们调用了其中的Thread.currmentThread()方法,可以直接打印线程的
名字。
下面介绍一下我们常用的Thread类中的的几种方法:
多线程的创建,为了对各个线程进行标识,他们有一个默认的名称,getName()方法
格式:Thread-从零角标开始编号
currentThread()返回当前线程对象的引用
那么想获取主函数的线程名怎么做呢?
Thread.currentThread.getName()获取线程的名字
那么既然能获取到线程的名字,我们能否设置线程的名字呢?
Thread(String name):构造函数,线程对象一建立就可以指定名称
2)在多运行代码几次之后我们会发现
每次打印的结果都不一样。这是为什么呢?
这是因为多个线程同时执行,而cpu在同一时间只能执行一个程序(多核除外),
线程想要运行就要抢夺cpu的执行权,谁抢到了谁就能执行。
这也是多线程的一个特点:随机性。谁抢到了执行权谁就可以执行,但是执行
多长时间cpu说了算。
总结一下继承Thread类有什么特点呢?
如果一个类明确了父类,那么就不能在在继承Thread类,继承了Thread类也不能在继承其他的类。这是Java继承中的 局限性,只能单继承。
2.实现Runnable接口
步骤:
1)创建一个类继承Runnable接口
2)复写run方法,将自定义的代码存放到run方法中。
3)创建继承Runnable接口的子类对象。
4)创建Thread类对象,然后将子类对象传递给Thread的构造方法。
5)start方法启动线程。
代码实现:
两种创建线程的方法我们都学完了,那么他们有什么区别呢?
1)继承Thread:线程代码存放Thread子类run方法中。
实现Runnable,线程代码存在接口的子类的run方法。
2)继承Thread:只能单继承
实现Runnable:可以多实现,即使类已经声明了父类也可以直接实现,因此建议大家使用Runnable实现。
同步(synchronized)代码块
在实现卖票的的功能中,如果细心的话我们会发现票是共享数据,这时候可能会想到用static修饰,这是一种实现方式。但是我们在前面有讲到,static修饰的声明周期过长,不建议大家使用。
在多运行售票程序几次后,我们会发现出现问题了:
冒出了0,-1,-1票的问题,
这是怎么回事呢?
这正是由于多线程的随机性造成的,因为线程有一个阻塞态,可能cpu执行线程到一半就切换到别的线程,一同来操控共享 数据,从而导致了共享数据的访问错误。这也是多线程的一个安全问题。
那该怎么解决呢?
如果我们一个数据被多条线程访问,那我们就干脆让这个数据在被一个线程访问时,不被其他线程访问即可。鉴于这种情 况的发生,Java提供的专业的解决方案,就是同步代码块。
1.定义同步(synchronized)代码块
定义格式:
synchronized(对象)
{
需要被同步的语句(看是哪些语句在操控共享数据)
}
其中对象就是如同一个锁,只有拿到锁的线程才可以执行同步代码块中的语句,因此必须保证锁的唯一性。
锁可以一个任意对象:
1)可以自己创建一个对象
2)可以是本类对象的引用this(同步函数)
3)可以是类被加载时的字节码对象(静态同步函数)
2.定义同步(synchronized)代码块的前提
1)必须要有两个或者两个以上的线程。
2)必须是多个线程使用同一个锁。
3.定义同步(synchronized)代码块的好处
好处:解决了多线程的安全问题
弊端:线程每次执行都要判断一下锁,比较消耗资源。
同步函数和同步代码块拥有相同的功能,定义同步函数的也很简单,在函数声明时加上aynchroized关键字修饰即可,在同步函数中包含需要被同步的代码,需要注意的是同步函数默认的锁是this.
代码示例:
6.静态同步函数
静态同步函数就是给同步函数加了个static修饰符。需要注意的是静态同步函数的对象为类被加载时的字节码对象。
格式为 类名.class 这里就不做代码演示了。
死锁(必须掌握)
死锁就是就同步中嵌套同步,而同步用的却不是一个锁,这时就会出现这个问题。
单例设计模式
在讲对象的时候我们提到过这个设计模式,是一道面试题,而且一般会考懒汉式。
懒汉式的考点:
1.懒汉式的延时加载可以节省资源。
2.懒汉式多线程时有安全隐患,可以用同步锁解决,但每次判断锁效率会比较低下。
3.在同步锁之前在加上一个判断,双重判断可解决效率问题。
1.什么是多线程?
在了解线程之前,我们先来了解一下什么是进程。在window系统中我们知道有任务管理器,在任务管理器可以操作系统中的进程。
那什么是进程呢?
进程:就是正在执行的程序,且每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者
叫做一个执行单元。
那这跟线程有什么关系呢?
线程:就是进程中一个独立的控制单,线程控制着进程的执行。
注意:
1)在一个进程中线程却可以有多个,这也是Java支持多线程技术的原因。
换句话说就是一个进程可以被多个控制单元(线程)操作。
2)一个进程至少有1个线程在控制。
3)java虚拟机启动时至少两个线程:一个是main主线程,一个是java.exe。
2.线程的生命周期
我们都知道程序运行都是有生命周期的,线程也不例外。
线程的生命周期分一下几种状态:被创建初始化状态---运行状态--阻塞状态--冻结状态(休眠态和等待态)---消亡状态
下面用图来表示一下这几种状态是如何转换的:
多线程的创建
在代码中如定义多线程?
通过查阅Java的API我们发现:
在Java中有两种定义多线程的方式,一个是继承Thread类一个是实现Runnable接口。
1.继承Thread类
步骤:
a.定义一个类继承Thread类
b.复写Thread类中的run方法,将自定义的代码存放到run方法中。
c.调用线程中的start方法,创建线程并执行run方法中的代码。
代码功能实现:
/*
创建两个线程 并同时运行
*/
class RunDemo
{
public static void main(String[] args)
{
Demo d1 = new Demo("one");
Demo d2 = new Demo("two");
d2.start();
//设置线程名字
//d1.setName("110");
d1.start();
for(int x=0;x<60;x++)
//currentThread方法返回当前正在执行线程对象的引用
System.out.println(Thread.currentThread().getName()+"。。。。"+"Hello World!..."+x);
}
}
//创建一个类集成Thread类
class Demo extends Thread
{
//private String name;
Demo(String name)
{
//this.name=name;
//构造函数设置线程名字
super(name);
}
//重写run方法
public void run()
{
for(int x=0;x<60;x++)
//getName方法 是返回当前执行线程的对象名称
System.out.println(this.getName()+"--"+"run test----"+x+this.getId());
}
}
1)在代码的运行时我们会发现
线程的名字被我们打印出来了,这是因为我们调用了其中的Thread.currmentThread()方法,可以直接打印线程的
名字。
下面介绍一下我们常用的Thread类中的的几种方法:
多线程的创建,为了对各个线程进行标识,他们有一个默认的名称,getName()方法
格式:Thread-从零角标开始编号
currentThread()返回当前线程对象的引用
那么想获取主函数的线程名怎么做呢?
Thread.currentThread.getName()获取线程的名字
那么既然能获取到线程的名字,我们能否设置线程的名字呢?
Thread(String name):构造函数,线程对象一建立就可以指定名称
2)在多运行代码几次之后我们会发现
每次打印的结果都不一样。这是为什么呢?
这是因为多个线程同时执行,而cpu在同一时间只能执行一个程序(多核除外),
线程想要运行就要抢夺cpu的执行权,谁抢到了谁就能执行。
这也是多线程的一个特点:随机性。谁抢到了执行权谁就可以执行,但是执行
多长时间cpu说了算。
总结一下继承Thread类有什么特点呢?
如果一个类明确了父类,那么就不能在在继承Thread类,继承了Thread类也不能在继承其他的类。这是Java继承中的 局限性,只能单继承。
2.实现Runnable接口
步骤:
1)创建一个类继承Runnable接口
2)复写run方法,将自定义的代码存放到run方法中。
3)创建继承Runnable接口的子类对象。
4)创建Thread类对象,然后将子类对象传递给Thread的构造方法。
5)start方法启动线程。
代码实现:
/*
需求:窗口卖票 多个窗口同时卖
思路:窗口买票是多个窗口同时买票
多个窗口就是多个线程
*/
class RunnableDemo
{
public static void main(String[] args)
{
//创建售票窗口对象
Ticket t = new Ticket();
//创建Thread对象
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
//启动线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
//定义一个类描述窗口买票 并实现Runnable接口
class Ticket implements Runnable
{
//定义票的数量
private int tic= 100;
//重写run方法
public void run()
{
while(true)
{
if(tic>0)
{
try
{
Thread.sleep(10);//让线程休眠10毫秒
}
catch(Exception e)
{
}
System.out.println(Thread.currentThread().getName()+"---tic----"+tic--);
}
}
}
}
两种创建线程的方法我们都学完了,那么他们有什么区别呢?
1)继承Thread:线程代码存放Thread子类run方法中。
实现Runnable,线程代码存在接口的子类的run方法。
2)继承Thread:只能单继承
实现Runnable:可以多实现,即使类已经声明了父类也可以直接实现,因此建议大家使用Runnable实现。
同步(synchronized)代码块
在实现卖票的的功能中,如果细心的话我们会发现票是共享数据,这时候可能会想到用static修饰,这是一种实现方式。但是我们在前面有讲到,static修饰的声明周期过长,不建议大家使用。
在多运行售票程序几次后,我们会发现出现问题了:
冒出了0,-1,-1票的问题,
这是怎么回事呢?
这正是由于多线程的随机性造成的,因为线程有一个阻塞态,可能cpu执行线程到一半就切换到别的线程,一同来操控共享 数据,从而导致了共享数据的访问错误。这也是多线程的一个安全问题。
那该怎么解决呢?
如果我们一个数据被多条线程访问,那我们就干脆让这个数据在被一个线程访问时,不被其他线程访问即可。鉴于这种情 况的发生,Java提供的专业的解决方案,就是同步代码块。
1.定义同步(synchronized)代码块
定义格式:
synchronized(对象)
{
需要被同步的语句(看是哪些语句在操控共享数据)
}
其中对象就是如同一个锁,只有拿到锁的线程才可以执行同步代码块中的语句,因此必须保证锁的唯一性。
锁可以一个任意对象:
1)可以自己创建一个对象
2)可以是本类对象的引用this(同步函数)
3)可以是类被加载时的字节码对象(静态同步函数)
2.定义同步(synchronized)代码块的前提
1)必须要有两个或者两个以上的线程。
2)必须是多个线程使用同一个锁。
3.定义同步(synchronized)代码块的好处
好处:解决了多线程的安全问题
弊端:线程每次执行都要判断一下锁,比较消耗资源。
4.售票系统代码优化示例
/*
需求:售票系统验证synchronized代码块
*/
class Ticket implements Runnable
{
private int tic=100;
//重写run方法
Object o = new Object();
public void run()
{
while(true)
{
synchronized(o)
{
if(tic>0)
// try{Thread.sleep(10);}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"---票---"+tic--);
}
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
Ticket tic = new Ticket();
new Thread(tic).start();
new Thread(tic).start();
new Thread(tic).start();
new Thread(tic).start();
}
}
5.同步函数
同步函数和同步代码块拥有相同的功能,定义同步函数的也很简单,在函数声明时加上aynchroized关键字修饰即可,在同步函数中包含需要被同步的代码,需要注意的是同步函数默认的锁是this.
代码示例:
6.静态同步函数
静态同步函数就是给同步函数加了个static修饰符。需要注意的是静态同步函数的对象为类被加载时的字节码对象。
格式为 类名.class 这里就不做代码演示了。
死锁(必须掌握)
死锁就是就同步中嵌套同步,而同步用的却不是一个锁,这时就会出现这个问题。
死锁说的简单点就是有两把锁,你想要我的,我也想要你的,谁给你不给谁,就在哪待着不懂抢锁玩了,这就是死锁。
代码示例:
class Test
{
public static void main(String[] args)
{
Thread t1 = new Thread(new Demo(true));
Thread t2 = new Thread(new Demo(false));
t1.start();
t2.start();
}
}
class Demo implements Runnable
{
private boolean flag;
Demo(boolean flag)
{
this.flag=flag;
}
public void run()
{
if(flag)
{
while(flag==true)
{
synchronized(Demo2.obja)//它的锁是obja
{
System.out.println("if-----obja");
synchronized(Demo2.objb)//它的锁是objb
{
System.out.println("if-----obja");
}
}
}
}
else
{
while(flag==false)
{
synchronized(Demo2.objb)//它的锁是objb
{
System.out.println("else-----obja");
synchronized(Demo2.obja)//它的锁是obja
{
System.out.println("else-----objb");
}
}
}
}
}
}
class Demo2
{
//定义两个锁
static Object obja = new Object();
static Object objb = new Object();
}
单例设计模式
在讲对象的时候我们提到过这个设计模式,是一道面试题,而且一般会考懒汉式。
懒汉式的考点:
1.懒汉式的延时加载可以节省资源。
2.懒汉式多线程时有安全隐患,可以用同步锁解决,但每次判断锁效率会比较低下。
3.在同步锁之前在加上一个判断,双重判断可解决效率问题。
//饿汉式
class Single
{
private final Single s = new Single();//加final更加严谨 因为s只能有一个字 干脆给值锁死
private Single(){}
public static Single getInstance()
{
return s;
}
}
//懒汉式 延迟加载
class Single implements Runnable//多线程的问题
{
private Single s = null;
private Single(){}
public static Single getInstance()
{
if(s==null)//多重判断提高效率 如果不为空不用再去判断锁
{
synchronized ()//同步代码块
{
if(s==null)
s= new Single();
}
}
return s;
}
}