进程与线程
进程就是程序的一次执行,而线程可以理解为进程中的执行的一段程序片段。每个进程都有独立的代码和数据空间(进程上下文);而线程可以看成是轻量级的进程。一般来讲(不使用特殊技术),同一进程所产生的线程共享同一块内存空间。
同一进程中的两段代码是不可能同时执行的,除非引入线程(实际也非同时执行,但表面上有同时执行的效果)。
多线程情况下,多个线程都获取CPU的执行权,CPU执行谁,就执行谁,在某个时间点,只有一个程序在运行(多核除外)CPU在做着快速的切换,已达到看上去是同事运行的效果。
进程:是一个正在运行的程序,每一个进程都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元;
线程:就是进程中执行的一个控制单元,线程控制着进程的执行。
一个进程至少有一个线程,JVM 启动的时候会有一个进程java.exe。该进程中至少一个线程负责java程序的执行,而且这个线程运行的代码在在main方法中,该线程为主线程。
多线程创建
创建多线程,一般有两种方式,通过java.lang.Thread类或者通过Runnable接口。
继承Thread类
1、继承Thread类,并重新Thread类中的run方法
2、创建并启动一个线程,调用start方法(启动线程、调用run方法)
public class MyThread extends Thread{
@Override
public void run() {
super.run();
for (int i = 0; i < 5; i++) {
System.out.println("线程:"+currentThread().getName()+" 执行 "+ i);
}
}
/**
* @param args
*/
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start(); //开启线程。并执行该线程的run方法
//mt.run(); //仅仅对象调用放到,而创建线程了,并没有运行
}
}
为什么要覆盖run方法?
Thread类用于描述线程
该类就定义了一个功能,用于存储线程要运行的代码,该存储功能就是run方法
Thread类中run方法,用于存储线程要运行的代码。
开启线程的目的,运行自己希望执行的操作,由于上述,所以需要复写run方法
实现接口Runnable
1、覆盖Runnable接口中run的方法
2、通过Thread类建立线程对象
3、将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数
4、调用Thread类的start方法,开启线程,实现Runnable接口子类的run方法
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("线程:"+Thread.currentThread().getName()+" 执行 "+ i);
}
}
/**
* @param args
*/
public static void main(String[] args) {
MyRunnable mt = new MyRunnable(); //实例化需要运行的对象(还没有创建方法)
// Thread t = new Thread(mt); //实例化Runnable对象
// t.start(); //启动线程
new Thread(mt).start(); //Thread(Runnable runnable) 对Runnable对象的实现
}
}
继承、实现对比
一个比较经典的例子,一个买票系统,有3个售票窗口,一定量的票,分别以上述方式实现,如下:
继承方式;
public class MyTicket extends Thread{
private int count = 5; //一定量的票数
private String name;
public MyTicket(String name){
this.name = name;
}
@Override
public void run() {
super.run();
while(count>0){
System.out.println(name+" 卖票 "+count--);
}
}
public static void main(String[] args) {
// MyTicket mt = new MyTicket("窗口1");
// mt.start();
new MyTicket("窗口1").start();
new MyTicket("窗口2").start();
new MyTicket("窗口3").start();
}
}
运行结果:
窗口2 卖票 5
窗口1 卖票 5
窗口1 卖票 4
窗口3 卖票 5
窗口1 卖票 3
窗口2 卖票 4
窗口1 卖票 2
窗口3 卖票 4
窗口1 卖票 1
窗口2 卖票 3
窗口2 卖票 2
窗口2 卖票 1
窗口3 卖票 3
窗口3 卖票 2
窗口3 卖票 1
实现接口Runnable方式:
public class MyTicket1 implements Runnable {
private int count = 5; //票数
@Override
public void run() {
while(count>0){
System.out.println(Thread.currentThread().getName()+ " 卖票 "+this.count--);
}
}
public static void main(String[] args) {
MyTicket1 my = new MyTicket1();
new Thread(my, "1号窗口").start();
new Thread(my, "2号窗口").start();
new Thread(my, "3号窗口").start();
}
}
执行结果:
1号窗口 卖票 5
1号窗口 卖票 3
1号窗口 卖票 2
1号窗口 卖票 1
2号窗口 卖票 4
实现接口Runnable比继承Thread类所具有的优势:
1):适合多个相同的程序代码的线程去处理同一个资源
2):可以避免java中的单继承的限制
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立。
采用静态变量,定义一个共享资源,修改继承方式中变量的定义为:
private static int count = 5; //一定量的票数
对应运行结果:
窗口2 卖票 4
窗口1 卖票 5
窗口3 卖票 3
窗口1 卖票 1
窗口2 卖票 2
用static定义票数时,static变量存在于JVM方法区共享数据,程序共享资源,能够实现资源的共享。
在实际操作中总票数存储于中心数据库,各售票点分布于各地,资源读取的时候就会出现多个售票点同时查询票数,买票的情况,简单的定义一个静态变量不能达到要求,因此,需要用到同步处理机制。
线程安全
同样是卖票问题,每次线程执行后有2S的sleep时间,运行结果可能会出现异常:
public class MyTicket1 implements Runnable {
private int count = 5; //票数
@Override
public void run() {
while(count>0){
try{Thread.sleep(2000);}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+ " 卖票 "+this.count--);
}
}
public static void main(String[] args) {
MyTicket1 my = new MyTicket1();
new Thread(my, "1号窗口").start();
new Thread(my, "2号窗口").start();
new Thread(my, "3号窗口").start();
}
}
运行结果:
2号窗口 卖票 5
1号窗口 卖票 3
3号窗口 卖票 4
2号窗口 卖票 2
1号窗口 卖票 1
3号窗口 卖票 0
2号窗口 卖票 -1
出现0,-1的原因:
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行一部分,还没有执行完。另一个线程参与进来执行,导致共享数据的错误。
解决原因:对多条操作共享数据的语句,只能让一个线程执行,执行过程中,只能执行一个线程,实现同步操作。
java对于多线程安全问题提供了保证代码同步的方式:同步代码块
synchronized(对象) //设定一个标志位(加锁)
{
//需要被同步的代码
}
具体实现:
public class MyTicket3 implements Runnable{
private static int tick = 10;
Object obj = new Object();
public void run()
{
for(int i=0;i<tick;i++){
synchronized(obj) //同步化
{
if(tick>0)
{
try{Thread.sleep(4000);}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+ " 卖票 "+this.tick--);
}
}
}
}
/**
* @param args
*/
public static void main(String[] args) {
MyTicket3 my = new MyTicket3();
new Thread(my, "1号窗口").start();
new Thread(my, "2号窗口").start();
new Thread(my, "3号窗口").start();
}
}
运行结果:
1号窗口 卖票 10
1号窗口 卖票 9
1号窗口 卖票 8
1号窗口 卖票 7
3号窗口 卖票 6
3号窗口 卖票 5
2号窗口 卖票 4
3号窗口 卖票 3
1号窗口 卖票 2
2号窗口 卖票 1
如何使用同步:
1、必须要有两个或两个以上的线程
2、必须是多个线程使用同一个锁
3、要确定加锁的范围(明确哪些代码是多线程运行代码,哪些共享数据,哪些代码调用共享数据的进行操作的)
一些比较经典的多线程同步问题,如生产者消费者问题,银行家问题等,都是采用同步的方式处理的。
PS:可能有错误,或不完善的地方,有待进一步研究与学习。