Даже один ЦП может выполнять «несколько задач одновременно» в широком смысле, но на самом деле они не параллельны. Вы можете запустить 100 потоков для выполнения на одном ядре, и они получат временные интервалы, в течение которых каждый из них может выполнять несколько инструкций, таким образом создавая впечатление, что все они выполняются одновременно.

Как я уже говорил в другом посте SO: многопоточность на двухъядерной машине?

Термин потоки обычно охватывает три уровня абстракции:

  1. потоки, запускаемые приложениями и отображаемые N:M на:
  2. , которые являются потоками, управляемыми операционной системой, сопоставленными N:M с:
  3. , которые являются фактически доступными физическими ресурсами.

Потоки Java — это пользовательские потоки. 4 ядра вашего процессора считаются аппаратными потоками. Поскольку отображение между слоями представляет собой N:M, вы можете видеть, что несколько пользовательских потоков могут быть сопоставлены с меньшим количеством аппаратных потоков.

Теперь, сказав это, обычно есть два класса действий потоков, каждый со своими особенностями:

  1. : эти потоки проводят большую часть своего времени в ожидании операций чтения/записи из потока и тем временем блокируются (их выполнение не запланировано до тех пор, пока не произойдет событие, которое их разбудит). На процессоре есть свет, и многие из них могут работать одновременно даже на одном ядре.
  2. : эти потоки много обрабатывают числа и максимально используют ЦП. Как правило, запуск большего количества (в 2 раза превышающего количество доступных ядер) таких потоков приведет к снижению производительности, поскольку ЦП имеет ограниченное количество функциональных блоков: ALU, FPU и т. д.

Второй класс потоков выше позволяет вам действительно увидеть преимущества запуска многопоточной Java-программы на вашем четырехъядерном процессоре. Вот простой пример программы, которая выполняет возведение в квадрат 1 000 000 000 чисел сначала последовательно, а затем параллельно, используя пул потоков с 4 потоками:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
class ThreadTask implements Runnable {
    private int total = 0;
    public ThreadTask(int total) {
        this.total = total;
    }
    @Override
    public void run() {
        int value = 0;
        for(int i = 0; i < total; i++) {
            value = i * i;
        }
    }       
}
public class Test {
    public static void main(String[] args) throws InterruptedException {
        int total = 1000000000;
        long start = System.currentTimeMillis();
        long value = 0;
        for(int i = 0; i < total; i++) {
            value = i * i;
        }       
        long stop = System.currentTimeMillis();
        System.out.println((stop - start) + " ms");
        ExecutorService exec = Executors.newFixedThreadPool(4);
        start = System.currentTimeMillis();
        for(int i = 0; i < 4; i++) {
            exec.submit(new ThreadTask(total / 4));
        }
        exec.shutdown();
        exec.awaitTermination(10, TimeUnit.SECONDS);
        stop = System.currentTimeMillis();
        System.out.println((stop - start) + " ms");     
    }
}

Не стесняйтесь изменять значение total, если оно работает слишком быстро. Сейчас работаю над нетбуком с Intel Atom, поэтому он не очень быстрый.

Подробнее о программировании на JAVA здесь.