1:多线程
(1)多线程:一个应用程序有多条执行路径;
进程:正在执行的应用程序;
线程:进程的执行单元,执行路径;
单线程:一个应用程序只有一条执行路径;
多线程:一个应用程序有多条执行路径;
多进程的意义:提高CPU的使用率;
多线程的意义:提高应用程序的使用率。
(2)Java程序的运行原理及JVM的启动时多线程的吗?
A:Java命令去启动JVM,JVM会启动一个进程,该进程会启动一个主线程。
B:JVM的启动是多线程的,因为它最少有两个线程启动了,主线程和垃圾回收线程。
(3)多线程的实现方案:
A:继承Thread类:
B:实现Runnable接口:
(4)一个简单的多线程实例(继承Thread类):
public class ThreadDemo extends Thread {
public ThreadDemo() {
}
public ThreadDemo(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + ":" + i);
}
}
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo("窗口一");
ThreadDemo td2 = new ThreadDemo("窗口二");
td.start();
td2.start();
}
}
//执行结果:(部分)
窗口一:0
窗口一:1
窗口二:0
窗口一:2
窗口二:1
窗口一:3
窗口一:4
窗口一:5
上诉演示的是带参构造函数实例化,无参函数实例化如下:
public class ThreadDemo extends Thread {
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 100; i++) {
System.out.println(getName() + ":" + i);
}
}
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo();
ThreadDemo td2 = new ThreadDemo();
td.start();
td2.start();
}
}
//执行结果:(部分)
Thread-0:0
Thread-1:0
Thread-0:1
Thread-1:1
Thread-0:2
Thread-1:2
Thread-0:3
实际上,可以发现,当使用的是无参函数实例化时,通过getName()函数,可以自动生成对应的线程名,由底层源码可以知道:
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
既然有了getName(),当然其对应的也存在setName(),底层使用name[]字符数组对线程的名字进行存储,因此无参的构造函数同样也可以給他起一个自己想要的名字:
td.setName("线程一");
td.setName("线程二");
那么我们可以知道的是Thread类的子类可以通过父类中getName()方法,得到自己本线程的名字,那么执行main方法的线程名字是什么呢?main方法是程序的入口,由JVM调用:Thread类提供了这样一个静态方法,来返回对当前正在执行的线程对象的引用。
public class ThreadDemo {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());
}
}
//执行结果:
main
(5)线程的调度:
假设CPU是一个的话,同一个时间CPU只能执行一条指令,线程只有得到CPU的使用权才能执行指令,那么线程是如何被调用的呢?Java又是采用的那种线程调度机制呢?
线程的调度模型有两种:分时调度模型和抢占式调度模型。
分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片。
抢占式调度模型:优先让优先级高的线程使用CPU。如果线程的优先级相同,那么会随机选择一个。优先级高的线程获取的CPU时间相对多一些。
Java采用的是抢占式调度模型。
(6)线程优先级:(线程的默认优先级为5)
获取线程的优先级:public final int getPriority()
更改线程的优先级:public final void setPriority(int newPriority)
最大优先级:public static final int MAX_PRIORITY :10
最小优先级:public static final int MIN_PRIORITY :1
默认优先级:public static final int NORM_PRIORITY:5
但需要注意的是:某个线程设置了优先级为最高,并不能保证说一定是他执行结束其他线程才能得到CPU,而是保证的是其获得CPU的可能性是最高的。
(7)线程控制:
A:休眠线程:public static void sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 100; i++) {
System.out.println(getName() + ":" + i);
try {
Thread.sleep(1000);//休眠时间设置为1s
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
//执行结果:
当该线程执行到这的时候就会休眠一秒
B:加入线程:public final void join():等待这个线程死亡。其他的线程才能开始得到CPU。
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo("窗口一");
ThreadDemo td2 = new ThreadDemo("窗口二");
td.start();
try {
td.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
td2.start();
}
//执行结果:
窗口一的线程任务执行结束后才执行窗口二的
C:礼让线程:public static void yield():暂停当前正在执行的线程对象,并执行其他线程。
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 100; i++) {
System.out.println(getName() + ":" + i);
Thread.yield();
}
}
礼让线程只能在一定程度上保证两个线程的有序性(多个线程的和谐性)。
D:守护线程:public final void setDaemon(boolean on):当运行的唯一线程都是守护进程线程时,Java虚拟机将退出。on - 如果 true
,将此线程标记为守护线程。当主线程结束后守护线程会随着主线程的结束而立即终止。但需要注意的是:在设置某个线程为守护线程的时候,要在启动前设置其为守护线程。
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo("窗口一");
ThreadDemo td2 = new ThreadDemo("窗口二");
td.setDaemon(true);
td.setDaemon(true);
td.start();
td2.start();
Thread.currentThread().setName("主窗口");
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
E:中断线程:
public final void stop():让线程停止,过时了。
public void interrupt():中断线程,把线程的状态终止,并抛出一个InterruptedException。
import java.util.Date;
public class ThreadDemo extends Thread {
public ThreadDemo(String name) {
super(name);
}
@Override
public void run() {
System.out.println("当前时间是:" + new Date());
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
System.out.println("线程被中断了");
}
System.out.println("当前时间是:" + new Date());
}
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo("窗口一");
td.start();
try {
Thread.sleep(3000);
td.interrupt();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
(8)线程的生命周期:
A:新建:创建线程对象
B:就绪:有执行资格但没有执行权
C:运行:有执行资格有执行权
阻塞:由于一些操作让线程处于这种状态,没有执行资格,没有执行权,但可以通过其他方式使其变为就绪状态。
D:死亡:线程对象变成垃圾等待回收
(9)一个简单的多线程实例(实现Runnable接口):
public class RunnableDemo implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
public static void main(String[] args) {
RunnableDemo rd = new RunnableDemo();
Thread t1 = new Thread(rd,"窗口一");
Thread t2 = new Thread(rd,"窗口二");
t1.start();
t2.start();
}
}
//注意这里Thread构造方法传进去的是Runnable接口作为参数
(10)讲了Runnable接口实现多线程后,就先作一个小结:总结一下继承Thread类和实现Runnable接口创建线程对象的步骤:
方式一:继承Thread类:
A:自定义类继承Thread类
B:在自定义类中重写run()方法
C:创建自定义类对象
D:启动线程对象
方式二:实现Runnable接口:
A:自定义类实现Runnable接口
B:重写run()方法
C:创建自定义类对象
D:创建Thread对象,并把C步骤的对象作为构造参数传递
有了方式一创建线程为什么还会有方式二呢?
1、可以避免由于Java单继承带来的局限性(举例:一个类已经继承了起父类,但又想实现多线程......)
2、适合多个相同程序的代码去处理同一个资源的情况,把线程相同程序的代码、数据进行有效的分离,较好的体现了面向对象的设计思想。(举例,当自定义类继承的是Thread类时,若自定义类中有成员变量,那么每实例化一次,该成员变量就多做一份拷贝,而如果实现的是Runnable接口,没创建一个对象,其传入的参数都是该实现Runnable接口的实现类的一个实例,副本当然也就一个,相当于对同一资源进行操作)。
(11)run()方法和start()方法有什么区别?
run()方法直接调用仅仅是普通方法;start()方法先是启动线程再由JVM调用run()方法。
(12)判断一个程序是否会出现线程安全问题:
A:看是否是多线程环境
B:看是否有共享数据
C:看是否有多条语句操作共享数据
满足以上任意一点都有可能出现多线程环境。那么如何解决线程安全问题呢?可以知道的是对于A,B两点,如果满足的话那么我们一般是无法改变的,我们只好想办法改变C。思想是:把多条语句操作共享数据的代码包成一个整体,让某一线程来执行的时候别人不能来执行。Java也提供了同步机制:
同步代码块:synchronized(对象){需要同步的代码块}
//同步机制实现简单的抢票功能
public class SellTicketRunnableDemo implements Runnable {
private int ticket = 100;
// 创建锁对象
private Object object = new Object();
@Override
public void run() {
while (true) {
synchronized (object) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第:" + ticket-- + "张票");
}
}
}
}
public static void main(String[] args) {
SellTicketRunnableDemo strd = new SellTicketRunnableDemo();
Thread t1 = new Thread(strd, "窗口一");
Thread t2 = new Thread(strd, "窗口二");
Thread t3 = new Thread(strd, "窗口三");
t1.start();
t2.start();
t3.start();
}
}
//注意的是:
给操作同一共享数据的所有操作代码上锁,该锁必须是同一把锁
同步的特点:前提是多线程。多线程使用的是同一个锁对象。
同步的好处:同步的出现解决了多线程的安全问题。
同步的弊端:当线程相当多时,因为每个线程都回去p-anduan同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。
(13)对于(12)点中的代码也能写成如下方式:
public class SellTicketRunnableDemo implements Runnable {
private int ticket = 100;
// 创建锁对象
private Object object = new Object();
@Override
public void run() {
while (true) {
SellTicket();
}
}
private void SellTicket() {
synchronized (object) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第:" + ticket-- + "张票");
}
}
}
public static void main(String[] args) {
SellTicketRunnableDemo strd = new SellTicketRunnableDemo();
Thread t1 = new Thread(strd, "窗口一");
Thread t2 = new Thread(strd, "窗口二");
Thread t3 = new Thread(strd, "窗口三");
t1.start();
t2.start();
t3.start();
}
}
哎呦,这个时候有人就想说,你把这个同步代码块提取到一个方法里面当然没有问题了, 线程一进入到方法就发现synchronized关键字,就知道同步了。的确,上诉的写法是没有任何问题的,该写法只是用来从同步代码块过渡到同步方法上去的。同步代码块的锁对象可以是任意对象,但是同步方法的锁对象又是什么呢???this。那么静态同步方法的锁对象又是谁呢?类的字节码文件对象(反射)。
(14)总结一下之前Java基础中讲到的线程安全的类:
A:Vector B:StringBuffer C:Hashtable
通过查看这些类的底层实现,可以知道这些类所有可调用的方法都加了synchronized关键字,保证了线程的安全。
当然集合工具类Collections也提供了方法供其他非线程安全的集合转化为线程安全的集合:
List<String>list=Collections.synchronizedList(new ArrayList<>());
//将线程不安全的ArrayList集合转化为线程安全的集合。
注:以上文章仅是个人学习过程总结,若有不当之处,望不吝赐教。