一、几个概念
1.进程
一个正在执行中的程序
一个正在执行中的程序
每个进程都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元
2.线程
进程中一个独立的控制单元,线程在控制着进程的执行,线程才是进程中真正执行的部分
进程中一个独立的控制单元,线程在控制着进程的执行,线程才是进程中真正执行的部分
一个进程中至少有一个线程,但是可以拥有多条执行路径,即多个线程
3.多线程
多个线程并发执行
3.多线程
多个线程并发执行
4.多线程存在的意义
多线程可以让程序产生同时运行效果,提高程序执行效率。
多线程可以让程序产生同时运行效果,提高程序执行效率。
以虚拟机为例,Java vm 启动的时候会有一个进程java.exe,该进程中至少有一个线程负责Java程序的执行,而且这个线程运行的代码存在main方法中,该线程称之为主线程。jvm启动时不止一个线程,除了主线程之外,还有一个线程专门负责垃圾回收机制。如果只有主线程没有垃圾负责垃圾回收的线程,一旦垃圾过多内存用完,程序将无法执行下去,抑或主线程停下来去处理垃圾,原本正在执行的程序将处于等待状态,影响效率。而多线程可以使多段代码同时执行,可以一边执行程序一边处理垃圾。
四、多线程安全问题
卖票的程序中,票据出现了0号票,现实生活中是不应该出现0号票,这里就涉及到了线程安全的问题
加入了同步的卖票的例子

可以看到没有0号票了
5.同步的好处与弊端
死锁产生的原因:
同步中嵌套同步
死锁的例子
程序运行结果截图

出现死锁,程序无法继续执行下去
二、创建线程的两种方式
第一种:继承Thread类
1.定义类继承Thread
2.覆写Thread类中的run()方法
3.调用线程的start方法
程序示例
运行截图
每一次的运行结果都不一样

程序示例
<span style="font-size:14px;"><span style="font-size:12px;">class SubThread extends Thread{
/**
* 创建线程类的子类,重写run()方法
*/
// String name;
public SubThread(String name){
// this.name = name;
super(name);
}
//覆写run方法
public void run(){
for(int i=1; i<=600; i++)
//输出线程名以及运行的次数
System.out.println(Thread.currentThread().getName()+"SubThread run----"+i);
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
SubThread st1 = new SubThread("one---");//创建一个线程
st1.start();//开启线程
SubThread st2 = new SubThread("two+++");//创建一个线程
st2.start();//开启线程
//主线程代码部分
for(int i=1; i<=600; i++)
System.out.println("hello world!--"+i);
}
}</span></span>
public static Thread currentThread() 获取当前线程对象
public String getName() 获取线程名称
public String getName() 获取线程名称
设置线程名称:setName方法或者构造函数
线程都有自己默认的名称,Thread-编号 该编号从0开始,如果不设置线程名就会采用这种默认的命名方式
运行截图
每一次的运行结果都不一样
第二种:实现Runnable接口
1.定义类实现Runnable接口
2.覆盖Runnable接口中的run方法
3.通过Thread类建立线程对象
4.将Runnale接口的子类对象作为实际参数传递给Thread类的构造函数
5.调用Thread类的start方法开启线程并调用Runnable接口子类的run方法
程序示例
运行结果截图

这里涉及到的线程安全的问题将在后面解释
两种方式中都覆写了run方法了,为什么要覆盖run方法?
既然run方法用于存储线程要运行的代码,为什么要不直接调用run方法而是调用start方法?
程序示例
<span style="font-size:14px;"><span style="font-size:12px;">/*
* 卖票的例子
*/
class Ticket implements Runnable{
private static int ticket = 100; //总票数
@Override
public void run(){
while(true){
if(ticket>0){
try {
Thread.sleep(10);//线程休眠10毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印当前线程名称以及售出的票
System.out.println(Thread.currentThread().getName()+"---sale--- "+ticket--);
}
}
}
}
public class TicketDemo {
public static void main(String[] args) {
//创建Runable子类的对象
Ticket ticket = new Ticket();
//创建Thread类,将Runnable接口的子类对象传给它
Thread t1 = new Thread(ticket);
Thread t2 = new Thread(ticket);
//开启线程
t1.start();
t2.start();
}
}</span></span>
运行结果截图
这里涉及到的线程安全的问题将在后面解释
两种方式中都覆写了run方法了,为什么要覆盖run方法?
Thread类用于描述线程,该类定义了一个功能,用于存储线程要运行的代码,该存储功能就是run方法,即run方法用于存储线程要运行的代码。复写run方法的目的是将自定义代码存储在run方法,让线程运行。
start方法有两个作用:启动线程,调用run()方法
调用start方法与run方法的区别
start用于开启线程并执行该线程的方法。注意:重复调用start方法程序会抛出异常
调用run与一般的对象调用没有区别,线程创建了,并没有运行,依然只有一个线程
实现方式方式的好处:避免了单继承的局限行,定义线程时,建议使用这种方式
两个程序每次运行的结果都不一样,这又是为什么呢?
这和CPU的执行原理有关。
因为多个线程都在获取CPU的执行权,CPU执行到谁,谁就运行。明确一点,在某一个时刻,只能有一个程序在运行(多核除外)。CPU在做着快速的切换,以达到看上去是同时运行的效果。我们可以形象的把多线程的运行行为看成是在互相抢夺CPU的执行权。

继承方式和实现方式的区别
继承Thread:线程代码存放在Thread子类的run方法中
实现Runnable:线程代码存放在Runnable接口子类run方法中
两个程序每次运行的结果都不一样,这又是为什么呢?
这和CPU的执行原理有关。
因为多个线程都在获取CPU的执行权,CPU执行到谁,谁就运行。明确一点,在某一个时刻,只能有一个程序在运行(多核除外)。CPU在做着快速的切换,以达到看上去是同时运行的效果。我们可以形象的把多线程的运行行为看成是在互相抢夺CPU的执行权。
这就是多线程的一个特性:随机性。谁抢到谁执行,至于执行多长时间,由CPU决定。
三、线程的几种状态
1.被创建:通过new关键字创建了Thread类(或其子类)的对象,等待被启动
1.被创建:通过new关键字创建了Thread类(或其子类)的对象,等待被启动
2.运行状态:具备运行资格和执行权
3.临时阻塞状态:具备运行资格但是没有执行权
4.冻结状态:线程因为调用sleep方法或者wait方法等进入阻塞,放弃CPU的执行权
5.消亡状态:run方法结束或者线程调用了stop方法
线程状态图卖票的程序中,票据出现了0号票,现实生活中是不应该出现0号票,这里就涉及到了线程安全的问题
产生的原因:
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行,导致共享数据的错误。
解决办法:
对多条操作共享数据的语句,只能让一个线程执行完,在执行过程中,其他线程不可以参与执行。
Java中提供的解决方法----同步
1.同步代码块
用法:
用法:
synchronized(对象){
//需要被同步的代码
}
同步代码块解决线程安全的原因在于这个对象,好比一把锁,每个线程要执行被同步的代码都必须获取此对象的锁,如果这个对象的锁被其他线程获取就必须等待其他对象释放这个锁。
<span style="font-size:14px;"><span style="font-size:12px;">/*
* 卖票的例子,已加入同步
*/
class Ticket2 implements Runnable{
private static int ticket = 100; //总票数
Object obj = new Object(); //用于同步的对象
@Override
public void run(){
while(true){
synchronized(obj){ //加入同步
if(ticket>0){
try {
Thread.sleep(20);//线程休眠10毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印当前线程名称以及售出的票
System.out.println(Thread.currentThread().getName()+"---sale--- "+ticket--);
}
}
}
}
}</span></span>
程序运行截图可以看到没有0号票了
2.同步函数
用法:
在函数返回值类型前加上synchronized关键字
在函数返回值类型前加上synchronized关键字
同步函数用的是哪一个锁?
函数需要被对象调用,那么函数都有一个所属对象引用,就是this,所以同步函数使用的锁是this
如果同步函数被静态修饰后,使用的锁又是什么呢?
静态函数使用的不是this锁,因为在静态方法中也不可以定义this。
静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码对象 类名.class ,该对象的类型是Class
静态的同步方法,使用的锁是该方法所在类的字节码对象 ,即类名.class
示例:
示例:
<span style="font-size:14px;"><span style="font-size:12px;">/*
* 懒汉式单例设计模式的安全问题
*/
public class Single {
private static Single s = null;
private Single(){}
public static Single getInstance(){
if(s == null)
synchronized(Single.class){ //加入同步,静态函数中使用的锁只能是类对应的字节码对象
if(s == null)
s = new Single();
}
return s;
}
}</span></span>
3.同步的前提:
1).必须要有两个或者两个以上的线程
2).必须是多个线程使用同一个锁
4.如何找到多线程中的安全问题
1).明确哪些代码是多线程运行代码
2).明确共享数据
3).明确多线程运行代码中哪些语句是操作共享数据的
1).明确哪些代码是多线程运行代码
2).明确共享数据
3).明确多线程运行代码中哪些语句是操作共享数据的
好处:解决了多线程的安全问题
弊端:多个线程都需要判断锁,较为消耗资源
五、死锁问题死锁产生的原因:
同步中嵌套同步
死锁的例子
<span style="font-size:14px;"><span style="font-size:12px;">
/*
* 死锁的例子
*/
class MyLock {
static Object lockA = new Object();
static Object lockB = new Object();
}
class Test implements Runnable{
private boolean flag;
public Test(boolean flag){
this.flag = flag;
}
@Override
public void run() {
if(flag){
synchronized(MyLock.lockA){ //上lockA锁
System.out.println("if lockA");
synchronized(MyLock.lockB){ //上lockB锁
System.out.println("if lockB");
}
}
}else{
synchronized(MyLock.lockB){ //上lockB锁
System.out.println("else lockB");
synchronized(MyLock.lockA){ //上lockA锁
System.out.println("else lockA");
}
}
}
}
}
public class DeadLockDemo {
public static void main(String[] args) {
Thread t1 = new Thread(new Test(true));
Thread t2 = new Thread(new Test(false));
t1.start();
t2.start();
}
}
</span></span>
程序运行结果截图
出现死锁,程序无法继续执行下去