首先弄清楚什么是进程?什么是线程?
进程:正在运行的一个程序。如打开任务管理器时,会看到正在运行的QQ,360等应用,每一个正在运行的应用程序就是一个进程。
线程:线程在进程里面,也可以说进程可以进一步细化为线程,是一个程序内部的一条执行路径。
若同一个进程同一时间并行执行多个线程,就是支持多线程的。
举个例子,打开360安全卫士后,可以选择一边杀毒的同时还可以扫描垃圾文件,其实这两个过程就是两个线程。
线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器。
单核CPU:其实是一种假的多线程,因为在一个时间单位内,也只能执行一个线程的任务。
多核CPU:能够更好的发挥多线程效率。
一个Java应用程序java.exe其实至少有三个线程:main()主线程,gc()垃圾回收机制,异常处理线程。
并行:多个CPU同时执行多个任务。
并发:一个CPU同时执行多个任务。
何时需要多线程?
多线程的创建,方式一:继承Thread类
步骤:
- 创建一个继承于Thread类的子类;
- 重写Thread类的run()方法;
- 创建该子类的对象;
- 通过此对象调用start()方法
例1:遍历10以内的所有偶数
class Mthread extends Thread
{
//重写Thread类的run()方法
public void run()
{
for (int i = 0; i <10 ; i++)
{
if (i % 2 == 0) {
System.out.println(i);
}
}
}
}
public class MyThread {
public static void main(String[] args) {
//创建该子类的对象
Mthread m=new Mthread(); //此时只是创建线程,但并未启动线程
//通过此对象调用start()方法
//此时有两个作用:1)启动当前线程;2)调用当前线程的run()
m.start();
//如下操作是在main线程中执行的
for (int i = 0; i <10; i++) {
System.out.println("主线程"+i);
}
}
}
注意:如果线程对象直接调用run(),即m.run(),此时只是普通的对象调用方法,并未启动线程。
例2:使用匿名内部类创建两个线程。
public class MyThread {
public static void main(String[] args) {
//使用匿名内部类来定义子线程
new Thread()
{
//重写run()
public void run() {
for(int i = 0; i <10 ; i++)
{
if (i % 2 == 0)
{
System.out.println(Thread.currentThread().getName()+"..."+i);
}
}
}
}.start();
new Thread(){
@Override
public void run() {
for (int i = 0; i < 10; i++)
{
if (i % 2 != 0)
{
System.out.println(Thread.currentThread().getName()+"..."+i);
}
}
}
}.start();
}
}
- start():启动当前线程,调用当前线程的run();
- run():通常需要重写Thread类中的此方法,将创建线程要执行的操作声明在此方法中;
- currentThread():静态方法,返回执行当前代码的线程;
- getName():获取当前线程名字;
- setName():设置当前线程名;
- yield():释放当前CPU的执行权;
- join():在线程A中调用线程B的join(),此时线程A就进入阻塞状态,直到线程B完全执行完以后,线程A才结束阻塞状态;
- stop():已过时。强制结束当前线程;
- sleep(long millitime):单位为毫秒,让当前线程“睡眠”指定的millitime毫秒。在此时间之内,当前线程是阻塞状态;
- isAlive():判断当前线程是否存活,返回ture或false
- 同优先级线程组成先进先出队列;
- 对高优先级,会抢占CPU的执行权;
- 线程的优先等级分为3个:MAX_PRIORITY:10 ,MIN_PRIORITY:1,NORM_PRIORITY:5;
- 如何获取和设置当前线程的优先级?
getPriority():获取线程的优先级
setPriority():设置线程的优先级
注意:高优先级的线程会抢占CPU的执行权,但这只是从概率上讲,并不意味着只有高优先级的线程执行完后才执行低优先级的线程。
- 定义类实现Runnable接口;
- 覆盖Runnable接口中的run方法,将线程要运行的代码存放在该run方法中;
- 通过Thread类建立线程对象;
- 将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数;
- 调用Thread类的start方法开启线程并调用Runnable接口子类的run方法
为什么要将Runnable接口的子类对象传递给Thread的构造函数?
因为,自定义的run方法所属的对象是Runnable接口的子类对象。
所以要让线程去指定对象的run方法。就必须明确该run方法所属对象.实现方式和继承方式创建线程有什么区别呢?
实现方式好处:避免了单继承的局限性。
在定义线程时,建议使用实现方式。两种方式区别:
继承Thread:线程代码存放Thread子类run方法中。
实现Runnable,线程代码存在接口的子类的run方法。
例3:简单的卖票程序,三个窗口同时卖票,总票数为100张。
class Window implements Runnable
{
private int ticket=100;
public void run()
{
while(true)
{
if(ticket>0)
{
System.out.println(Thread.currentThread().getName() + "卖票:" + ticket);
ticket--;
}
else
{
break;
}
}
}
}
public class MyThread {
public static void main(String[] args) {
Window w=new Window(); //此时只new()了一个对象,所以一共卖100张票
Thread t1=new Thread(w); //创建了一个线程
Thread t2=new Thread(w); //创建了一个线程
Thread t3=new Thread(w); //创建了一个线程
t1.start();
t2.start();
t3.start();
}
}
新建:使用new关键字创建Thread类或及其子类对象后,该线程就处于新建状态。此时,通过对象调用start()方法后,线程进入就绪状态。
就绪:此时线程已经具备了运行条件,但是还没有分配到CPU的执行权,处于线程就绪队列,等待系统为其分配CPU。一旦获得了CPU的执行权,那么线程就进入运行状态,并自动调用自己的run()方法。
运行:此时线程执行自己的run()方法,直到调用其他方法而终止,或等待某资源而阻塞或完成任务而死亡。
阻塞:处于运行状态的线程在某些情况下,如执行了sleep()方法后,此时将让出cpu的执行权并停止自己的运行。只有当引起阻塞的原因消除时,如睡眠时间已到,此时线程便转入就绪状态,再次等待cpu的执行权。其实阻塞状态时线程具备运行资格,但没有cpu执行权。
死亡:死亡状态是线程生命周期的最后一个阶段。线程死亡有两个原因:一是正常运行的线程完成了全部工作;二是一个线程被强制性终止,如通过stop()。
发现例3中的卖票问题,会出现重票错票的问题,此时多线程的运行出现了安全问题。
出现安全问题的原因:
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,
另一个线程参与进来执行。导致共享数据的错误。
解决办法:
对多条操作共享数据的语句,只能让一个线程都执行完。在执行过程中,其他线程不可以参与执行。
Java对于多线程的安全问题提供了专业的解决方式。
就是同步代码块。
格式:
synchronized(同步监视器)
{
需要被同步的代码(操作共享数据的代码)
}
同步监视器:俗称,锁。任何一个类的对象都可以充当锁。
持有锁的线程可以在同步中执行。
没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁。
同步的前提:
1,必须要有两个或者两个以上的线程。
2,多个线程必须使用同一个锁。
而且必须保证同步中只能有一个线程在运行。
好处:解决了多线程的安全问题。
弊端:多个线程需要判断锁,较为消耗资源。
那么修改以后的卖票程序为:
class Window implements Runnable
{
private int ticket=100;
Object obj=new Object();
public void run()
{
while(true)
{
//同步代码块
synchronized(obj) {
if (ticket > 0) {
//就算此时sleep()其他的线程也进不来,因为没有获得锁
try
{
Thread.sleep(10);
}
catch(Exception e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖票:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
public class MyThread {
public static void main(String[] args) {
Window w=new Window(); //此时只new()了一个对象,所以一共卖100张票
Thread t1=new Thread(w); //创建了一个线程
Thread t2=new Thread(w); //创建了一个线程
Thread t3=new Thread(w); //创建了一个线程
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
我们发现使用同步代码块以后的不安全问题解决了。
- 同步函数使用的锁是this;
- 对于static的同步函数,使用的锁不是this。是 类名.class 是该类的字节码文件对象
例:使用同步方法解决实现Runnable接口的线程安全问题
class Window implements Runnable
{
private int ticket=100;
Object obj=new Object();
public void run()
{
while(true)
{
show();
}
}
private synchronized void show() //此时同步函数使用的锁是this
{
if (ticket > 0)
{
try
{
Thread.sleep(10);
}
catch(Exception e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖票:" + ticket);
ticket--;
}
}
}
public class MyThread {
public static void main(String[] args) {
Window w=new Window(); //此时只new()了一个对象,所以一共卖100张票
Thread t1=new Thread(w); //创建了一个线程
Thread t2=new Thread(w); //创建了一个线程
Thread t3=new Thread(w); //创建了一个线程
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
如果同步函数被静态修饰后,使用的锁是什么呢?
通过验证,发现不在是this。因为静态方法中也不可以定义this。
静态进内存是,内存中没有本类对象,但是一定有该类对应的字节码文件对象。
类名.class 该对象的类型是Class
其实:静态的同步方法,使用的锁是该方法所在类的字节码文件对象。 类名.class
使用同步方法解决继承Thread类的线程安全问题
class Window extends Thread
{
private static int ticket=100;
public void run()
{
while(true)
{
show();
}
}
private static synchronized void show() //注意此时对于静态同步方法中使用的锁是 类名.class
{
if (ticket > 0)
{
try
{
Thread.sleep(10);
}
catch(Exception e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖票:" + ticket);
ticket--;
}
}
}
public class MyThread {
public static void main(String[] args) {
Window w1=new Window();
Window w2=new Window();
Window w3=new Window();
w1.setName("窗口1");
w2.setName("窗口2");
w3.setName("窗口3");
w1.start();
w2.start();
w3.start();
}
}
例:解决单例设计模式中的懒汉式的线程安全问题。
懒汉式特点:延迟加载。
不足:当多线程访问时会出现安全问题。如何解决?:可以采用加同步的方式解决。
加同步的方式有同步函数和同步代码块,都低效。但是用双重判断的方式可以解决效率问题。
加同步是锁是:该类所在的字节码文件
先说一下饿汉式为:
class Single
{
private static Single s=new Single();
private Single(){}
private static Single getInstance()
{
return s;
}
}
采用加同步的方式解决懒汉式:
class Single {
private static Single s = null;
private Single() {
}
public static Single getInstance() {
synchronized (Single.class) //锁是该类的字节码文件
{
if (s == null)
s = new Single(); //对象的延迟加载
}
return s;
}
}
上述的方法虽然使用了同步方式,但是效率仍然会很低。
现在使用双重判断的方式可以解决效率问题。
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;
}
}
这样就可以解决效率低的问题了。原因在于,第一个线程创建了对象之后,之后的每一个线程再进来时不会再判断锁了,直接拿已经建立好的对象来使用,提高了效率。
- 什么是死锁? 同步中嵌套同步;
- 出现死锁后,不会出现异常,也不会出现提示,只是所有的线程都处于阻塞状态,无法继续,程序摆在那里了;
- 我们使用同步时应避免出现死锁。
死锁的例子:
class Test implements Runnable
{
private boolean flag;
Test(boolean flag)
{
this.flag = flag;
}
public void run()
{
if(flag)
{
while(true)
{ //锁locka中有lockb锁
synchronized(MyLock.locka)
{
System.out.println(Thread.currentThread().getName()+"...if locka ");
synchronized(MyLock.lockb)
{
System.out.println(Thread.currentThread().getName()+"..if lockb");
}
}
}
}
else
{
while(true)
{ //锁lockb中有locka锁
synchronized(MyLock.lockb)
{
System.out.println(Thread.currentThread().getName()+"..else lockb");
synchronized(MyLock.locka)
{//对于static的同步函数,使用的锁不是this。是 类名.class 是该类的字节码文件对象
System.out.println(Thread.currentThread().getName()+".....else locka");
}
}
}
}
}
}
class MyLock
{
static Object locka = new Object();
static Object lockb = new Object();
}
class DeadLockTest
{
public static void main(String[] args)
{
Thread t1 = new Thread(new Test(true));
Thread t2 = new Thread(new Test(false));
t1.start();
t2.start();
}
}