进程与线程
进程是正在运行的程序,是系统进行资源分配和调用的独立单位,每一个进程都有它自己的内存空间和系统资源
线程是进程中单个顺序控制流,是一条执行路径,根据执行路径的数量不同,分为单线程和多线程
多线程的实现方式
方法一:继承Thread类(不常用)
- 继承
Thread
类 - 重写
run
方法
两个问题:
为什么要重写run方法?
因为run方法是用来封装被线程执行的代码的
run方法和start方法的区别?
- run():封装线程执行的代码,直接调用,相当于普通方法
- start():启动线程,然后由JVM调用此线程的run()方法
//继承Thread类
public class MyThread extends Thread {
//重写run方法
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
public static void main(String[] args) {
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
//发现仍然是单线程,因为直接调用run方法其实并没有启动线程
//myThread1.run();
//myThread2.run();
myThread1.start();
myThread2.start();
}
}
方法二:实现Runnable接口
- 实现Runnable接口
- 实现run方法
- 创建Thread的时候当作参数传进入
public class MyRunnable implements Runnable {
//重写run方法
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread1 = new Thread(myRunnable,"猴子");
Thread thread2 = new Thread(myRunnable,"大象");
//使用匿名内部类的形式
Thread thread3 = new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
};
thread3.setName("老虎");
thread1.start();
thread2.start();
thread3.start();
}
}
实现Runnable接口的好处
- 避免了Java单继承的局限性
- 适用于多个相同程序的代码去处理同一个资源的情况,把线程和程序代码、数据有效分离开,体现了面向对象的设计思想
- 而且通过实现接口可以使用匿名内部类轻松创建
设置和获取线程的名称
Thread类提供了两个方法:
- setName()
- getName()
super(name)
:通过构造方法往上传
获取当前正在执行的线程:
- Thread提供了
currentThread()
静态方法
线程优先级
Java实现的是抢占式调度模型,即优先级高的线程可以获得的CPU时间片段多一点。除此之外还有分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片
- getPriority()获取线程优先级
- setPriority()设置线程优先级
//继承Thread类
public class MyThread extends Thread {
//重写run方法
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
public static void main(String[] args) {
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
MyThread myThread3 = new MyThread();
//输出了3个5,即默认为5
System.out.println(myThread1.getPriority());
System.out.println(myThread2.getPriority());
System.out.println(myThread3.getPriority());
//设置优先级,越大越高
myThread1.setPriority(11);
myThread1.start();
myThread2.start();
myThread3.start();
}
}
线程控制函数
方法 | 说明 |
---|---|
sleep() | 让当前线程暂停指定毫秒数 |
join() | 等待这个线程执行完,剩下的程序才会执行 |
setDaemon(boolean) | 标记为守护线程 |
注:当运行的线程都是守护线程时,JVM将退出
线程生命周期
线程数据同步
多线程的同步问题指的是多个线程同时修改一个数据的时候,可能导致的问题 ,多线程的问题,又叫Concurrency
问题,解决方法是把操作共享数据的代码锁起来,让任意时刻只能有一个线程执行
问题出现的原因:
- 多线程环境
- 有共享数据
- 有多条执行语句操作共享数据
代码格式:相当于给代码加锁,任意对象可以看成一把锁
synchronized (任意对象) {
多条语句操作共享数据的代码
}
public class MyRunnable implements Runnable {
private int number = 100;
private Object obj = new Object();
//重写run方法
@Override
public void run() {
while (true) {
synchronized (obj) {
if (number>0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(number);
number--;
}
}
}
}
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread1 = new Thread(myRunnable, "窗口1");
Thread thread2 = new Thread(myRunnable, "窗口2");
thread1.start();
thread2.start();
}
}
数据同步的好处和弊端
- 好处:解决了多线程数据安全问题
- 弊端:当线程很多的时候,会浪费资源,导致程序效率很低
同步方法(加了synchronized的方法)
同步方法就是让synchronized
修饰的方法,同步方法的锁对象是this
,代码格式如下:
public synchronized void test(){
//todo
}
静态同步方法的锁是类名.class
,即字节码对象
public static synchronized void test(){
//todo
}
线程安全类
Hashtable不能存放null,HashMap可以存放 null
Hashtable是线程安全的类,HashMap是非线程安全的
StringBuffer 是线程安全的,StringBuilder 是非线程安全的,StringBuilder 更快且常用,因为不执行同步
Vector是线程安全的类,而ArrayList是非线程安全的,ArrayList更快且常用,因为不执行同步
concurrent包
concurrent
包提供了一些高效的映射、有序集合、队列的实现:
ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、ConcurrentLinkedQueue
把非线程安全的集合转换为线程安全
借助Collections.synchronizedList,可以把ArrayList转换为线程安全的List,与此类似的,还有HashSet,LinkedList,HashMap等等非线程安全的类
List<Integer> list2 = Collections.synchronizedList(list1);
Lock锁
Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作,有两个常用方法:
- lock()获得锁
- unlock()释放锁
Lock是接口,不能直接实例化,常采用它的实现类ReentrantLock
来实例化,新版本代码如下
public class MyRunnable implements Runnable {
private int number = 100;
private Lock lock = new ReentrantLock();
//重写run方法
@Override
public void run() {
while (true) {
try {
lock.lock();
if (number>0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(number);
number--;
}
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread1 = new Thread(myRunnable, "窗口1");
Thread thread2 = new Thread(myRunnable, "窗口2");
thread1.start();
thread2.start();
}
}
注:通常情况下使用try...finally
来包裹操作,防止中间出现问题无法释放锁
生产者-消费者模式介绍
生产者生产数据放到共享数据区域,消费者从共享数据区域获取数据
方法 | 说明 |
---|---|
wait() | 让当前线程等待,直到另一个线程调用notify()或notifyall() |
notify() | 唤醒正在等待的单个线程 |
notifyall() | 唤醒正在等待的所有线程 |
注:包含了这些方法的关键字应该有synchronized
进行修饰,否则会报错,如下:
public synchronized void put(int milk) {
if (state) {
try {
wait();//等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
生产者-消费者模式代码
奶箱:
public class Box {
//表示剩下几瓶奶
private int milk;
//表示奶箱的状态
private boolean state = false;
//生产奶
public synchronized void put(int milk) {
if (state) {
try {
wait();//如果有牛奶应该是等待消费
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//没有牛奶再生产
this.milk = milk;
System.out.println("送奶工将第"+this.milk+"放入奶箱");
state = true;
//唤醒
notify();
}
//消费奶
public synchronized void get() {
if (!state) {
//没有牛奶就等
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果有牛奶就消费
System.out.println("用户拿到第"+this.milk+"奶");
state = false;
//唤醒
notify();
}
}
生产者:
public class Productor implements Runnable {
private Box b;
public Productor(Box b) {
this.b = b;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
b.put(i);
}
}
}
消费者:
public class Customer implements Runnable {
private Box b;
public Customer(Box b) {
this.b = b;
}
@Override
public void run() {
while (true) {
b.get();
}
}
}
测试类:
public class BoxTest {
public static void main(String[] args) {
Box box = new Box();
Productor productor = new Productor(box);
Customer customer = new Customer(box);
Thread thread1 = new Thread(productor);
Thread thread2 = new Thread(customer);
thread1.start();
thread2.start();
}
}
死锁的产生
- 线程1 首先占有对象1,接着试图占有对象2
- 线程2 首先占有对象2,接着试图占有对象1
- 线程1 等待线程2释放对象2
- 与此同时,线程2等待线程1释放对象1
就会。。。一直等待下去,直到天荒地老,海枯石烂,山无棱 ,天地合。。。
原子访问
所谓的原子性操作
即不可中断的操作
,比如赋值操作,原子性操作本身是线程安全的
,但是i++
这个行为,事实上是有3个原子性操作组成
的:
- 步骤 1. 取 i 的值
- 步骤 2. i + 1
- 步骤 3. 把新的值赋予i
JDK6 以后,新增加了一个包java.util.concurrent.atomic
,里面有各种原子类,比如AtomicIntege
而AtomicInteger提供了各种自增,自减等方法,这些方法都是原子性的,同一个时间,只有一个线程可以调用这个方法。
public class TestThread {
public static void main(String[] args) throws InterruptedException {
AtomicInteger atomicI =new AtomicInteger();
int i = atomicI.decrementAndGet();
int j = atomicI.incrementAndGet();
int k = atomicI.addAndGet(3);
}
}
线程池
每一个任务都去执行一个新的线程,开销还是很大,因此有了线程池,将任务丢到线程池中去执行即可
目前还用不到,等有需要会补充。