线程和进程的概念
什么是进程?
进程就是正在运行的应用程序,是系统进行资源分配和调用的一个独立单位。每一个进程都有它自己的内存空间和系统资源。
一般来说,进程包括以下三个特征:
1、独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
2、动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中就扔了时间的概念。进程拥有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的。
3、并发性:多个进程可以在单个处理器上并发执行,多个进程间不会互相影响
多进程有什么意义?
单进程的计算机只能做一件事情。现在的计算机都支持多进程的。
多进程可以在一个时间段内执行多个任务,提高CPU的使用率。
一边玩游戏,一边听音乐是同时进行的吗?
不是。因为单CPU在某一个时间点上只能做一件事情。而我们在玩游戏,或者听音乐的时候,是CPU在做着程序间的高效切换,让我们以为它们是同时进行的。
什么是线程?
一个进程内可执行多个任务,这每一个任务就可以看成一个线程。一个进程有多个线程。线程是程序的执行单元,是一条执行路径。是程序使用CPU的最基本单位。
单线程程序:一个进程只有一条执行路径。
多线程程序:一个进程有多条执行路径。
多线程有什么意义?
不是提高程序的执行速度。是提高应用程序的使用率。
程序的执行其实都是在抢CPU的资源,CPU的执行权。多个线程都在抢这个资源,而如果其中的某一个进程的执行路径比较多,就会有更高的几率抢到CPU的执行权。
线程的执行具有随机性,因为我们不确定哪一个线程在哪一个时刻抢到这个资源。
并发和并行?
并行:逻辑上同时发生,指在某一个时间内同时运行多个程序。
并发:物理上同时发生,指在某一个时间点同时运行多个程序。
java程序运行原理是什么?
java命令会启动 java虚拟机(java virtual machine即JVM),JVM启动就相当于启动了一个应用程序,也就是启动了一个进程。该进程会自动启动一个”主线程”,然后主线程去调用某个类的main方法。所以main方法运行在主线程中,在此之前的所有程序都是单线程的。
JVM虚拟机的启动时单线程还是多线程的?
多线程的。因为垃圾回收线程也要先启动,否则很容易出现内存溢出。垃圾回收线程和主线程,最低启动了两个线程,所以是多线程的。
多线程的优势:
当操作系统创建一个进程时,必须为该进程分配独立的内存空间,并分配大量的相关资源。但创建一个线程则简单的多。
进程之间不能共享数据,单线程之间共享内存非常容易。
系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高。
java语言内置了多线程功能的支持,而不是单纯地作为底层操作系统的调度方式,从而简化了java的多线程变成
多线程创建的四种方式
http://blog.csdn.net/qq_36748278/article/details/78143998
创建线程的几种方式的比较
为什么要重写run()方法?
因为不是类中所有的代码都要被线程执行,为了区分哪些代码能够被线程执行,Thread类中的run()方法只是包含那些被线程执行的代码。一般来说,被线程执行的代码是比较耗时的。
run()和start()的区别是什么?
run():仅仅是封装被线程执行的代码,直接调用仅仅是普通方法。
start():首先启动线程,然后由JVM去调用该线程的run()方法。
采用实现Runnable接口的方式有什么好处?
通过实现接口的方式解决了java单继承带来的局限性
适合多个相同的程序代码去处理同一个资源的情况,把线程同程序的代码以及数据进行有效分离,较好的体现了面向对象的设计思想。
public class MyThread extends Thread {
private String myParam = null;
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(this.getName() + ":" + i);
}
}
}
MyThread myThread = new MyThread("这是第一个线程");
MyThread myThread2 = new MyThread("这是第二个线程"); //有几个就new几次
MyRunnable mr = new MyRunnable(); //只new了一次
Thread t1 = new Thread(mr,"小骨");
Thread t2 = new Thread(mr,"玥公子");
如果MyThread类中有别的成员变量,他就会创建很多次myParam变量。而通过实现接口的方式,他只new了一次,然后被重复利用,适合多个相同的程序代码去处理同一个资源的情况。
异常
IllegalThreadStateException:非法的线程状态异常。同一个线程不能被调用两次。需要创建两个线程。
方法
public final String getName():获取线程的名称
public final void setName(String name):设置线程的名称
public static Thread currentThread():返回当前正在执行的线程对象
线程调度
线程的两种调度模型?
分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片。
抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片多一些。
java采用的是抢占式调度模型。
线程的优先级?
线程优先级高仅仅表示线程获取的CPU时间片的几率高,但是要在次数比较多或者多次运行的情况下效果才明显。
public final int getPriority():获取线程对象的优先级。线程默认的优先级都是5
public final void setPriority(int newPriority):更改线程的优先级
public static final int MAX_PRIORITY:线程可以具有的最高优先级。值为10
public static final int NORM_PRIORITY:分配给线程的默认优先级。值为5
public static final int MIN_PRIORITY:线程可以具有的最低优先级。值为1
IllegalArgumentException异常:向方法传递了一个不合法或不正确的参数
线程类:
public class ThreadPriority extends Thread {
@Override
public void run() {
for(int i = 0;i < 100;i++){
System.out.println(getName() + ":" + i);
}
}
}
测试类:
public class ThreadPriorityDemo {
public static void main(String[] args) {
ThreadPriority tp = new ThreadPriority();
ThreadPriority tp1 = new ThreadPriority();
ThreadPriority tp2 = new ThreadPriority();
tp.setName("老大");
tp1.setName("老二");
tp2.setName("老三");
//获取默认优先级
System.out.println(tp.getPriority()); //5
System.out.println(tp1.getPriority()); //5
System.out.println(tp2.getPriority()); //5
System.out.println(Thread.MAX_PRIORITY); //最高优先级:10
System.out.println(Thread.MIN_PRIORITY); //最低优先级:1
System.out.println(Thread.NORM_PRIORITY); //默认优先级:5
tp.start();
tp1.start();
tp2.start();
}
}
线程控制
线程休眠: public static void sleep(long miliis)
线程加入: public final void join()。使用join()方法的线程中止之后才能执行后续的线程。
ThreadJoin类:
public class ThreadJoin extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + ":" + i);
}
}
}
测试类:
public class ThreadJoinDemo {
public static void main(String[] args) {
ThreadJoin ts = new ThreadJoin();
ThreadJoin ts1 = new ThreadJoin();
ThreadJoin ts2 = new ThreadJoin();
ts.setName("爸爸"); // 有了爸爸,之后才可能有老大和老二
ts1.setName("老大");
ts2.setName("老二");
ts.start();
try {
ts.join(); //老大和老二,必须等爸爸执行完了才能执行
} catch (InterruptedException e) {
e.printStackTrace();
}
ts1.start();
ts2.start();
}
}
线程礼让: public static void yield()。
暂停当前线程对象,并执行其他线程。让多个线程执行更和谐,但是不能靠它保证一人一次。
后台线程: public final void setDaemon(boolean on)
将该线程标记为守护线程或用户线程。守护线程是守护被守护的线程的,被守护的线程关闭之后,守护线程也要关闭。当正在运行的线程都是守护线程时,java虚拟机退出。该方法必须在启动前调用。
ThreadDaemon类:
public class ThreadDaemon extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + ":" + i);
}
}
}
测试类:
public class ThreadDaemonDemo {
public static void main(String[] args) {
ThreadDaemon ts = new ThreadDaemon();
ThreadDaemon ts1 = new ThreadDaemon();
ts.setName("左膀,守护将军");
ts1.setName("右臂,守护将军");
// 把ts和ts1都设置为守护线程,当主线程不存在的时候,他们两个守护线程也不能存在了
ts.setDaemon(true);
ts1.setDaemon(true);
ts.start();
ts1.start();
Thread.currentThread().setName("将军"); // 将军不在了,左膀右臂也不能存在了
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);;
}
}
}
中断线程
public final void stop():让线程停止,已经过时了。具有不安全性,不建议使用
public void interrupt():中断线程,把线程状态终止,并抛出一个InterruptedException。也就是说走了异常,后面的代码还是会执行。
ThreadInterrupt类:
public class ThreadInterrupt extends Thread {
@Override
public void run() {
System.out.println("开始执行:"+new Date());
//休息10秒钟
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
System.out.println("线程被终止了");
}
System.out.println("结束执行:"+new Date());
}
}
测试类:
public class ThreadInterruptDemo {
public static void main(String[] args) {
ThreadInterrupt ts = new ThreadInterrupt();
ts.start();
//超过3秒钟不醒过来,就结束线程
try {
Thread.sleep(3000);
ts.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
控制台输出:
开始执行:Sun Sep 24 15:29:51 CST 2017
线程被终止了
结束执行:Sun Sep 24 15:29:54 CST 2017
线程的生命周期
线程的一些状态:
1、新建:创建线程对象
2、就绪:线程有执行资格,没有执行权
3、运行:有执行资格,有执行权
4、阻塞:由于一些操作让线程改变了状态,没有执行资格,没有执行权
另一些操作可以把它给激活,激活处于就绪状态
5、死亡:线程对象变成垃圾,等待被回收
用多线程实现卖电影票实例
通过使用Runnable接口的方式:
SellTicket类:
public class SellTicket implements Runnable {
private int tickets = 100; //通过实现接口的方式,这个类只会被创建一次,因此这个变量也是被共享的。
@Override
public void run() {
while (true) {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
}
}
}
}
测试类:
public class SellTicketDemo {
public static void main(String[] args) {
//创建资源对象
SellTicket st = new SellTicket();
//创建3个线程对象
Thread t1 = new Thread(st,"窗口1");
Thread t2 = new Thread(st,"窗口2");
Thread t3 = new Thread(st,"窗口3");
t1.start();
t2.start();
t3.start();
}
}
在真实生活中,售票时网络是不能实时传输的,总是存在延迟的情况,所以,在出售一张票以后,需要一点时间的延迟。因此我们采用每次卖票延迟100毫秒来模拟真实的情况:
修改SellTicket类:
public class SellTicket implements Runnable {
private int tickets = 100;
@Override
public void run() {
while (true) {
if (tickets > 0) {
//为了模拟更真实的场景,在这里sleep:100ms
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
}
}
}
}
控制台输出错误事例情况之一:只贴出了错误信息:
窗口2正在出售第1张票
窗口3正在出售第0张票
窗口1正在出售第-1张票
通过加入延迟后,就发生了两个问题:
1、相同的票卖了多次:CPU的每一次执行必须是一个原子性(最简单基本的)操作。
(1)假设此时tickets=100
(2)因为当A线程进入了if(tickets>0)之后,它会睡眠
(3)此时B线程进来了,它进入了if(tickets>0),之后它也睡眠
(4)然后A线程醒了,它卖了第100张票。
(5)CPU的每一次执行必须是一个原子性(最简单基本的)操作。但是tickets– 并不是原子性操作。
a:它先记录以前的值
b:然后tickets–
c:然后输出以前的值
d:tickets的值变成99。
(6)如果上面的操作一次性做完,是正常的,但是如果上面的操作做了一半,然后B线程醒了,此时tickets还没变成99,那么也就是说B线程也卖了第100张票。因此就出现了相同的票卖了多次的现象。
2、出现了负票
(1)假设现在tickets = 1
(2)A线程进来了if(tickets > 0)并休息,B线程也进来了也休息,C线程也进来了也休息。
(3)A 正在出售第1张票,tickets = 0;
B 正在出售第0张票,tickets = -1;
C 正在出售第-1张票,tickets = -2;
这就是我们所说的线程安全问题。下面我将会讲解如何解决这些线程安全问题。
线程安全问题
在什么情况下会出现线程安全问题?当同时满足以下三个情况的时候,我们的程序就会出现问题。
(1)是否是多线程环境?是
(2)是否有共享数据?是
(3)是否有多条语句操作共享数据?是
如何解决多线程安全问题?
基本思想:让程序没有安全问题的环境。
也就是说把多个语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可。
多线程安全是什么?
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。
同步解决线程安全问题:
方式一、同步代码块:同步代码块的锁对象是任意对象。多个线程的锁必须是一把锁。这个对象也必须使用公用的。
synchronized(任意对象){
需要同步的代码
}
卖电影票的解决方法:
public class SellTicket implements Runnable {
private int tickets = 100;
//创建锁对象。(保证所有的线程都是用的同一把锁)
private Object obj = new Object();
@Override
public void run() {
while (true) {
//同步代码块
synchronized (obj) {
if (tickets > 0) {
//为了模拟更真实的场景,在这里sleep:100ms
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
}
}
}
}
}
方式二、同步方法:把同步关键字加在方法上。锁对象是this
卖电影票的解决方法:
public class SellTicket implements Runnable {
private int tickets = 100;
@Override
public void run() {
while (true) {
if(tickets%2 == 0){
//可以知道,同步方法的锁对象是this
synchronized (this) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
}
}
}else{
sellTicket();
}
}
}
private synchronized void sellTicket(){
if (tickets > 0) {
//为了模拟更真实的场景,在这里sleep:100ms
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
}
}
}
方式三、静态方法:把同步关键字加在方法上。锁对象是类的字节码文件
卖电影票的解决方法:
public class SellTicket implements Runnable {
private static int tickets = 100;
@Override
public void run() {
while (true) {
if(tickets%2 == 0){
//可以知道,静态方法的锁对象是类的字节码文件对象
synchronized (SellTicket.class) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
}
}
}else{
sellTicket();
}
}
}
private static synchronized void sellTicket(){
if (tickets > 0) {
//为了模拟更真实的场景,在这里sleep:100ms
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
}
}
}
JDK5之后的Lock锁。(一般情况下使用synchronized)
虽然我们可以通过同步代码块和同步方法添加锁对象,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
但是我们可以发现:Lock锁对象其实是一个接口。
java.util.concurrent.locks :接口 Lock
所有已知实现类: ReentrantLock, ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock
下面还是用卖票的例子来看看Lock对象是怎么使用的:
SellTicket类:
public class SellTicket implements Runnable {
private int tickets = 100;
// 定义锁对象
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
// 加锁
lock.lock();
if (tickets > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在卖第" + tickets-- + "张票");
}
} finally {
// 释放锁(放在try-finally中:防止前面出问题之后,就没办法释放锁。通过放在finally之后,无论发生什么情况都会释放锁)
lock.unlock();
}
}
}
}
测试类:
public class SellTicketDemo {
public static void main(String[] args) {
SellTicket st = new SellTicket();
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
同步思想
同步的前提:有多个线程,并且多个线程使用的是同一个锁对象。
同步的好处:同步的出现解决了多线程的安全问题
同步的弊端:
(1)当线程非常多的时候,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。
(2)容易产生死锁。
死锁问题
死锁:两个或两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待现象。
Lock类:创建两个锁对象
public class MyLock {
//创建两把锁对象
public static final Object objA = new Object();
public static final Object objB = new Object();
}
死锁代码:MyLock类
public class DieLock extends Thread {
private boolean flag;
public DieLock(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
synchronized (MyLock.objA) {
System.out.println("if objA");
synchronized (MyLock.objB) {
System.out.println("if objB");
}
}
}else{
synchronized (MyLock.objB) {
System.out.println("else objB");
synchronized (MyLock.objA) {
System.out.println("else objA");
}
}
}
}
}
测试类:
public class DieLockDemo {
public static void main(String[] args) {
DieLock dl1 = new DieLock(true);
DieLock dl2 = new DieLock(false);
dl1.start();
dl2.start();
}
}
控制台输出:我们可以发现出现了互相等待的情况,也就是死锁现象
else objB
if objA
常用的线程安全的类
线程安全的类(都有synchronized修饰,线程安全,效率低)
StringBuffer
Vector
Hashtable
如何把一个线程不安全的集合类变成一个线程安全的集合类:
public static List synchronizedList(List list)
List list1 = Collections.synchronizedList(new ArrayList());